@pdpp/cli 0.1.0-beta.2 → 0.1.0-beta.3
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 +49 -5
- package/package.json +2 -2
- package/src/cache-layout.js +7 -4
- package/src/collector/commands.js +94 -0
- package/src/collector/errors.js +7 -0
- package/src/collector/runner.js +193 -0
- package/src/connect/flow.js +114 -4
- package/src/index.js +115 -2
- package/src/ref/args.js +47 -0
- package/src/ref/commands/connectors.js +94 -0
- package/src/ref/commands/grant.js +29 -0
- package/src/ref/commands/login.js +167 -0
- package/src/ref/commands/run.js +29 -0
- package/src/ref/commands/trace.js +29 -0
- package/src/ref/errors.js +37 -0
- package/src/ref/fetch.js +75 -0
- package/src/ref/output.js +76 -0
- package/src/ref/session.js +107 -0
package/README.md
CHANGED
|
@@ -4,21 +4,64 @@ 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 beta CLI
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
This package is the public npm home for the `pdpp` command. The beta CLI
|
|
8
|
+
supports three command namespaces:
|
|
9
|
+
|
|
10
|
+
- **`pdpp connect <provider-url>`** — delegated access: discovers provider
|
|
11
|
+
metadata, self-registers a public client when the AS advertises dynamic
|
|
12
|
+
registration, asks the owner to approve scoped access in the browser, and
|
|
13
|
+
stores scoped client credentials in the project-local `.pdpp/` cache without
|
|
14
|
+
asking for an owner bearer token.
|
|
15
|
+
|
|
16
|
+
- **`pdpp collector <advertise|enroll|run>`** — operator surface for the
|
|
17
|
+
local collector runner. Pairs a host the operator controls (Claude Code or
|
|
18
|
+
Codex CLI data) with a remote PDPP reference deployment via device-scoped
|
|
19
|
+
enrollment, then runs connectors that the provider/control-plane container
|
|
20
|
+
cannot run on its own. The runner ships separately as
|
|
21
|
+
`@pdpp/local-collector` and owns the `pdpp-local-collector` binary; `pdpp
|
|
22
|
+
collector ...` is a slim `@pdpp/cli` shim that resolves that package lazily.
|
|
23
|
+
Public onboarding should use `npx -y @pdpp/local-collector ...` or
|
|
24
|
+
`npm i -g @pdpp/local-collector` unless the operator intentionally wants the
|
|
25
|
+
`@pdpp/cli` shim.
|
|
26
|
+
|
|
27
|
+
- **`pdpp ref ...`** — reference operator diagnostics over `_ref` routes on a
|
|
28
|
+
running reference deployment. Current subcommands: `pdpp ref run timeline
|
|
29
|
+
<run-id>`, `pdpp ref grant timeline <grant-id>`, `pdpp ref trace show
|
|
30
|
+
<trace-id>`. Requires `PDPP_OWNER_SESSION_COOKIE` when owner auth is enabled.
|
|
31
|
+
These are reference-only operator tools, not core PDPP protocol.
|
|
12
32
|
|
|
13
33
|
## Install
|
|
14
34
|
|
|
15
35
|
```bash
|
|
36
|
+
# @pdpp/cli package, npx-launched pdpp binary
|
|
16
37
|
npx -y @pdpp/cli@beta --help
|
|
17
38
|
```
|
|
18
39
|
|
|
19
40
|
Use the `beta` dist-tag until PDPP intentionally enables stable `latest`
|
|
20
41
|
publication.
|
|
21
42
|
|
|
43
|
+
When working from this monorepo without installing or linking the binary, use
|
|
44
|
+
the workspace executable:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# @pdpp/cli package, workspace-launched pdpp binary
|
|
48
|
+
pnpm exec pdpp ref run timeline <run-id>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The public command surface is still the `pdpp` binary; `pnpm exec` is only the
|
|
52
|
+
local workspace launcher.
|
|
53
|
+
|
|
54
|
+
The local collector runtime is a separate public package:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# @pdpp/local-collector package, npx-launched pdpp-local-collector binary
|
|
58
|
+
npx -y @pdpp/local-collector advertise
|
|
59
|
+
|
|
60
|
+
# @pdpp/local-collector package, installs the pdpp-local-collector binary
|
|
61
|
+
npm i -g @pdpp/local-collector
|
|
62
|
+
pdpp-local-collector advertise
|
|
63
|
+
```
|
|
64
|
+
|
|
22
65
|
## Ownership And Publishing
|
|
23
66
|
|
|
24
67
|
The intended npm scope is `@pdpp`, owned by the durable PDPP/Vana project
|
|
@@ -32,6 +75,7 @@ After the package exists on npm, configure the trusted publisher with npm CLI
|
|
|
32
75
|
11.5.1+:
|
|
33
76
|
|
|
34
77
|
```bash
|
|
78
|
+
# npm trust command for the @pdpp/cli package publisher config
|
|
35
79
|
npm trust github @pdpp/cli --repo vana-com/pdpp --file semantic-release.yml
|
|
36
80
|
npm trust list @pdpp/cli
|
|
37
81
|
```
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pdpp/cli",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.3",
|
|
4
4
|
"description": "Command-line tools for PDPP providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"pdpp": "
|
|
7
|
+
"pdpp": "bin/pdpp.js"
|
|
8
8
|
},
|
|
9
9
|
"exports": {
|
|
10
10
|
".": "./src/index.js",
|
package/src/cache-layout.js
CHANGED
|
@@ -5,13 +5,16 @@ export function getPdppCacheLayout(cacheRoot = '.pdpp') {
|
|
|
5
5
|
return {
|
|
6
6
|
root: cacheRoot,
|
|
7
7
|
clientsDir: join(cacheRoot, 'clients'),
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
accessFile: join(cacheRoot, 'agent-access.json'),
|
|
11
|
-
secretFile: (name) => join(cacheRoot, 'secrets', `${name}.secret`),
|
|
8
|
+
gitignoreFile: join(cacheRoot, '.gitignore'),
|
|
9
|
+
credentialFile: (providerUrl) => join(cacheRoot, 'clients', `${providerCacheKey(providerUrl)}.json`),
|
|
12
10
|
};
|
|
13
11
|
}
|
|
14
12
|
|
|
13
|
+
function providerCacheKey(providerUrl) {
|
|
14
|
+
const host = providerUrl.includes('://') ? new URL(providerUrl).host : providerUrl;
|
|
15
|
+
return host.replace(/[^a-zA-Z0-9.-]/g, '_');
|
|
16
|
+
}
|
|
17
|
+
|
|
15
18
|
export function writePdppSecretFile(path, value) {
|
|
16
19
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
17
20
|
writeFileSync(path, value, { mode: 0o600 });
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { PDPP_CLI_BIN_NAME } from '../package-info.js';
|
|
2
|
+
import { CollectorUsageError } from './errors.js';
|
|
3
|
+
import { spawnCollectorRunner } from './runner.js';
|
|
4
|
+
|
|
5
|
+
const COLLECTOR_HELP = `Local collector runner (reference operator surface).
|
|
6
|
+
|
|
7
|
+
Pair a host you control with a PDPP reference deployment, then run
|
|
8
|
+
filesystem-class or local-device connectors that the provider container
|
|
9
|
+
cannot run on its own. Runner-owned flags are documented by
|
|
10
|
+
"pdpp-local-collector --help" (see "Distribution" below).
|
|
11
|
+
|
|
12
|
+
Distribution:
|
|
13
|
+
The collector runtime ships in @pdpp/local-collector, a separate npm
|
|
14
|
+
package owned by the PDPP monorepo. @pdpp/cli stays slim and resolves
|
|
15
|
+
the runner lazily:
|
|
16
|
+
# @pdpp/local-collector package, installs pdpp-local-collector
|
|
17
|
+
npm i -g @pdpp/local-collector
|
|
18
|
+
# @pdpp/local-collector package, npx-launched pdpp-local-collector
|
|
19
|
+
npx -y @pdpp/local-collector advertise
|
|
20
|
+
See openspec/changes/publish-pdpp-local-collector/design.md.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
${PDPP_CLI_BIN_NAME} collector advertise
|
|
24
|
+
${PDPP_CLI_BIN_NAME} collector enroll --base-url <url> --code <one-time-code>
|
|
25
|
+
[--device-label <label>]
|
|
26
|
+
${PDPP_CLI_BIN_NAME} collector run --base-url <url> --connector <id>
|
|
27
|
+
--device-id <id> --device-token <token>
|
|
28
|
+
--connection-id <id>
|
|
29
|
+
[--streams a,b,c]
|
|
30
|
+
[--backfill-streams attachments]
|
|
31
|
+
[--run-id <id>]
|
|
32
|
+
|
|
33
|
+
Suggested operator flow:
|
|
34
|
+
1. Start the reference deployment somewhere reachable (e.g. Docker on a
|
|
35
|
+
server) so it has a base URL such as http://server.local:7662.
|
|
36
|
+
2. Confirm runtime capabilities with:
|
|
37
|
+
${PDPP_CLI_BIN_NAME} collector advertise
|
|
38
|
+
(@pdpp/cli pdpp shim, resolving @pdpp/local-collector)
|
|
39
|
+
The collector advertises network, filesystem, local_device
|
|
40
|
+
and reports collector_protocol_version.
|
|
41
|
+
3. Mint an enrollment code from the dashboard or "pdpp ref" tooling,
|
|
42
|
+
then on the host with Claude/Codex data run:
|
|
43
|
+
${PDPP_CLI_BIN_NAME} collector enroll --base-url <url> --code <code>
|
|
44
|
+
(@pdpp/cli pdpp shim, resolving @pdpp/local-collector)
|
|
45
|
+
The JSON response returns device_id, device_token, and
|
|
46
|
+
source_instance_id (the connection id for this local binding) —
|
|
47
|
+
persist all three to a secrets store. You will pass them back as
|
|
48
|
+
flags/env vars in step 4.
|
|
49
|
+
4. Run a connector with:
|
|
50
|
+
PDPP_LOCAL_DEVICE_ID=<device_id> \\
|
|
51
|
+
PDPP_LOCAL_DEVICE_TOKEN=<device_token> \\
|
|
52
|
+
PDPP_CONNECTION_ID=<connection_id> \\
|
|
53
|
+
${PDPP_CLI_BIN_NAME} collector run --base-url <url> \\
|
|
54
|
+
--connector claude_code
|
|
55
|
+
(@pdpp/cli pdpp shim, resolving @pdpp/local-collector)
|
|
56
|
+
Connectors that need bindings the collector does not advertise fail
|
|
57
|
+
before spawn with "runtime_capability_mismatch".
|
|
58
|
+
|
|
59
|
+
Notes:
|
|
60
|
+
Collector credentials are device-scoped; they cannot read records,
|
|
61
|
+
approve grants, or mint owner tokens. Do not log device tokens. See
|
|
62
|
+
openspec/changes/publish-pdpp-local-collector/design.md.
|
|
63
|
+
Required flags can also be supplied via PDPP_REFERENCE_BASE_URL,
|
|
64
|
+
PDPP_LOCAL_DEVICE_ID, PDPP_LOCAL_DEVICE_TOKEN, PDPP_CONNECTION_ID,
|
|
65
|
+
PDPP_RUN_ID. PDPP_SOURCE_INSTANCE_ID remains a compatibility alias.
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
const SUBCOMMANDS = new Set(['advertise', 'enroll', 'run']);
|
|
69
|
+
|
|
70
|
+
export async function runCollector(argv, io) {
|
|
71
|
+
const [sub, ...rest] = argv;
|
|
72
|
+
|
|
73
|
+
if (!sub || sub === '--help' || sub === '-h' || sub === 'help') {
|
|
74
|
+
io.stdout.write(COLLECTOR_HELP);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!SUBCOMMANDS.has(sub)) {
|
|
79
|
+
io.stderr.write(`Unknown collector subcommand: ${sub}\n\n${COLLECTOR_HELP}`);
|
|
80
|
+
return 64;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
return await spawnCollectorRunner(sub, rest);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error instanceof CollectorUsageError) {
|
|
87
|
+
io.stderr.write(`${error.message}\n`);
|
|
88
|
+
return error.exitCode;
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { COLLECTOR_HELP };
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
import { CollectorUsageError } from './errors.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the published `@pdpp/local-collector` package, if installed.
|
|
11
|
+
*
|
|
12
|
+
* The shim prefers an installed `@pdpp/local-collector` so an operator who
|
|
13
|
+
* `npm i -g @pdpp/cli && npm i -g @pdpp/local-collector` can run
|
|
14
|
+
* `pdpp collector ...` without a monorepo checkout. Resolution is lazy —
|
|
15
|
+
* the CLI does NOT declare a runtime dependency on `@pdpp/local-collector`
|
|
16
|
+
* (per `publish-pdpp-local-collector` task 4.4); a missing package is
|
|
17
|
+
* surfaced as an actionable install hint rather than a hard import error.
|
|
18
|
+
*
|
|
19
|
+
* Spec: openspec/changes/publish-pdpp-local-collector/design.md §1.
|
|
20
|
+
*/
|
|
21
|
+
export function resolveLocalCollectorPackage(startDir = dirname(fileURLToPath(import.meta.url))) {
|
|
22
|
+
// Primary resolution: Node module resolution from the caller. Works for an
|
|
23
|
+
// npm install where @pdpp/local-collector is alongside @pdpp/cli in the
|
|
24
|
+
// same node_modules tree.
|
|
25
|
+
try {
|
|
26
|
+
const require = createRequire(join(startDir, '_'));
|
|
27
|
+
const manifestPath = require.resolve('@pdpp/local-collector/package.json');
|
|
28
|
+
return { manifestPath, packageDir: dirname(manifestPath) };
|
|
29
|
+
} catch {
|
|
30
|
+
// Continue to workspace fallback.
|
|
31
|
+
}
|
|
32
|
+
// Fallback: walk up the directory tree looking for a sibling
|
|
33
|
+
// packages/local-collector workspace. Preserves the monorepo dev flow
|
|
34
|
+
// where pnpm does not hoist workspace packages into @pdpp/cli's local
|
|
35
|
+
// node_modules (per the slim-CLI invariant in task 4.4).
|
|
36
|
+
let cursor = resolve(startDir);
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
while (!seen.has(cursor)) {
|
|
39
|
+
seen.add(cursor);
|
|
40
|
+
const candidate = join(cursor, 'packages', 'local-collector', 'package.json');
|
|
41
|
+
if (existsSync(candidate)) {
|
|
42
|
+
return { manifestPath: candidate, packageDir: dirname(candidate) };
|
|
43
|
+
}
|
|
44
|
+
const parent = dirname(cursor);
|
|
45
|
+
if (parent === cursor) {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
cursor = parent;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Locate the in-monorepo collector-runner TypeScript entrypoint.
|
|
55
|
+
*
|
|
56
|
+
* The shim's resolution order is:
|
|
57
|
+
*
|
|
58
|
+
* 1. monorepo workspace walk — preserves the current dev flow when
|
|
59
|
+
* `pdpp` is invoked from inside a checkout, which uses the
|
|
60
|
+
* filesystem-only `bin/collector-runner.ts` directly;
|
|
61
|
+
* 2. resolved `@pdpp/local-collector` package (via
|
|
62
|
+
* `resolveLocalCollectorPackage`);
|
|
63
|
+
* 3. fail-fast with a one-line install hint.
|
|
64
|
+
*
|
|
65
|
+
* This function only handles step 1; the higher-level `spawnCollectorRunner`
|
|
66
|
+
* weaves the order together so behavior is deterministic across
|
|
67
|
+
* monorepo + npm install postures.
|
|
68
|
+
*/
|
|
69
|
+
export function resolveCollectorRunnerScript(startDir = dirname(fileURLToPath(import.meta.url))) {
|
|
70
|
+
let cursor = resolve(startDir);
|
|
71
|
+
const seen = new Set();
|
|
72
|
+
while (!seen.has(cursor)) {
|
|
73
|
+
seen.add(cursor);
|
|
74
|
+
const candidate = join(cursor, 'packages', 'polyfill-connectors', 'bin', 'collector-runner.ts');
|
|
75
|
+
if (existsSync(candidate)) {
|
|
76
|
+
return candidate;
|
|
77
|
+
}
|
|
78
|
+
const parent = dirname(cursor);
|
|
79
|
+
if (parent === cursor) {
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
cursor = parent;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function resolveTsxBinary(startDir = dirname(fileURLToPath(import.meta.url))) {
|
|
88
|
+
let cursor = resolve(startDir);
|
|
89
|
+
const seen = new Set();
|
|
90
|
+
while (!seen.has(cursor)) {
|
|
91
|
+
seen.add(cursor);
|
|
92
|
+
const candidate = join(cursor, 'node_modules', '.bin', 'tsx');
|
|
93
|
+
if (existsSync(candidate)) {
|
|
94
|
+
return candidate;
|
|
95
|
+
}
|
|
96
|
+
const parent = dirname(cursor);
|
|
97
|
+
if (parent === cursor) {
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
cursor = parent;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* One-line install hint surfaced when neither the monorepo nor an
|
|
107
|
+
* installed `@pdpp/local-collector` can be found.
|
|
108
|
+
*/
|
|
109
|
+
const RUNNER_MISSING_MESSAGE =
|
|
110
|
+
'pdpp collector requires @pdpp/local-collector. Install once with ' +
|
|
111
|
+
'"npm i -g @pdpp/local-collector" or run "npx -y @pdpp/local-collector ...". ' +
|
|
112
|
+
'See openspec/changes/publish-pdpp-local-collector/design.md.';
|
|
113
|
+
|
|
114
|
+
const TSX_MISSING_MESSAGE =
|
|
115
|
+
'Could not locate tsx alongside the collector runner. Install ' +
|
|
116
|
+
'@pdpp/local-collector with "npm i -g @pdpp/local-collector" or run ' +
|
|
117
|
+
'"pnpm install" at the monorepo root.';
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Spawn the collector-runner subprocess. Inherits stdio so operators see
|
|
121
|
+
* device tokens, run results, and diagnostics directly. Returns the exit
|
|
122
|
+
* code, never throws on non-zero exits.
|
|
123
|
+
*
|
|
124
|
+
* Resolution order, locked in by `publish-pdpp-local-collector` design §1:
|
|
125
|
+
* 1. monorepo `bin/collector-runner.ts` if walking up the FS finds one;
|
|
126
|
+
* 2. published `@pdpp/local-collector` bin if installed;
|
|
127
|
+
* 3. fail-fast `RUNNER_MISSING_MESSAGE`.
|
|
128
|
+
*/
|
|
129
|
+
export async function spawnCollectorRunner(
|
|
130
|
+
subcommand,
|
|
131
|
+
argv,
|
|
132
|
+
{
|
|
133
|
+
env = process.env,
|
|
134
|
+
runnerScript = resolveCollectorRunnerScript(),
|
|
135
|
+
localCollector = resolveLocalCollectorPackage(),
|
|
136
|
+
tsxBinary = resolveTsxBinary(),
|
|
137
|
+
spawnFn = spawn,
|
|
138
|
+
stdio = 'inherit',
|
|
139
|
+
} = {},
|
|
140
|
+
) {
|
|
141
|
+
if (runnerScript) {
|
|
142
|
+
if (!tsxBinary) {
|
|
143
|
+
throw new CollectorUsageError(TSX_MISSING_MESSAGE);
|
|
144
|
+
}
|
|
145
|
+
return await runSubprocess(spawnFn, tsxBinary, [runnerScript, subcommand, ...argv], { env, stdio });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (localCollector) {
|
|
149
|
+
const binPath = resolveLocalCollectorBin(localCollector.packageDir);
|
|
150
|
+
if (!existsSync(binPath)) {
|
|
151
|
+
throw new CollectorUsageError(
|
|
152
|
+
`@pdpp/local-collector is installed at ${localCollector.packageDir} but is missing its bin entrypoint. ` +
|
|
153
|
+
'Reinstall the package or report this on https://github.com/vana-com/pdpp/issues.',
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
if (binPath.endsWith('.ts')) {
|
|
157
|
+
if (!tsxBinary) {
|
|
158
|
+
throw new CollectorUsageError(TSX_MISSING_MESSAGE);
|
|
159
|
+
}
|
|
160
|
+
return await runSubprocess(spawnFn, tsxBinary, [binPath, subcommand, ...argv], { env, stdio });
|
|
161
|
+
}
|
|
162
|
+
return await runSubprocess(spawnFn, process.execPath, [binPath, subcommand, ...argv], { env, stdio });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
throw new CollectorUsageError(RUNNER_MISSING_MESSAGE);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function resolveLocalCollectorBin(packageDir) {
|
|
169
|
+
try {
|
|
170
|
+
const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'));
|
|
171
|
+
const bin = manifest?.bin?.['pdpp-local-collector'];
|
|
172
|
+
if (typeof bin === 'string' && bin.trim()) {
|
|
173
|
+
return join(packageDir, bin);
|
|
174
|
+
}
|
|
175
|
+
} catch {}
|
|
176
|
+
const publishedBin = join(packageDir, 'dist', 'local-collector', 'bin', 'pdpp-local-collector.js');
|
|
177
|
+
if (existsSync(publishedBin)) return publishedBin;
|
|
178
|
+
return join(packageDir, 'bin', 'pdpp-local-collector.ts');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function runSubprocess(spawnFn, binary, args, { env, stdio }) {
|
|
182
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
183
|
+
const child = spawnFn(binary, args, { env, stdio });
|
|
184
|
+
child.on('error', rejectPromise);
|
|
185
|
+
child.on('exit', (code, signal) => {
|
|
186
|
+
if (signal) {
|
|
187
|
+
rejectPromise(new Error(`collector-runner terminated by signal ${signal}`));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
resolvePromise(code ?? 0);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
}
|
package/src/connect/flow.js
CHANGED
|
@@ -53,11 +53,24 @@ export async function connectProvider(providerUrl, options = {}) {
|
|
|
53
53
|
);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
const
|
|
56
|
+
const publicClient = await getOrRegisterPublicClient({
|
|
57
|
+
fetchFn,
|
|
58
|
+
authorizationMetadata,
|
|
59
|
+
cacheRoot,
|
|
60
|
+
providerUrl: normalizedProviderUrl,
|
|
61
|
+
clientName: 'PDPP CLI',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const startRequest = {
|
|
57
65
|
resource: normalizedProviderUrl,
|
|
58
66
|
scope,
|
|
59
67
|
client_name: 'PDPP CLI',
|
|
60
|
-
}
|
|
68
|
+
};
|
|
69
|
+
if (publicClient?.client_id) {
|
|
70
|
+
startRequest.client_id = publicClient.client_id;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const start = await postJson(fetchFn, connectEndpoint, startRequest);
|
|
61
74
|
|
|
62
75
|
const approvalUrl = start.approval_url ?? start.verification_uri_complete ?? start.verification_uri;
|
|
63
76
|
const pollUrl = start.poll_url ?? start.token_url ?? start.device_poll_endpoint ?? start.completion_endpoint;
|
|
@@ -87,6 +100,7 @@ export async function connectProvider(providerUrl, options = {}) {
|
|
|
87
100
|
provider_url: normalizedProviderUrl,
|
|
88
101
|
authorization_server: authorizationServerUrl,
|
|
89
102
|
scope,
|
|
103
|
+
client: publicClient,
|
|
90
104
|
credential,
|
|
91
105
|
created_at: new Date((options.now?.() ?? Date.now())).toISOString(),
|
|
92
106
|
});
|
|
@@ -100,9 +114,50 @@ export async function connectProvider(providerUrl, options = {}) {
|
|
|
100
114
|
authorizationServerUrl,
|
|
101
115
|
cacheFile,
|
|
102
116
|
scope,
|
|
117
|
+
clientId: publicClient?.client_id ?? null,
|
|
103
118
|
};
|
|
104
119
|
}
|
|
105
120
|
|
|
121
|
+
export async function readStoredCredential(providerUrl, options = {}) {
|
|
122
|
+
const normalizedProviderUrl = normalizeProviderUrl(providerUrl);
|
|
123
|
+
if (!normalizedProviderUrl) {
|
|
124
|
+
throw new ConnectError('invalid_provider_url', `Invalid provider URL: ${providerUrl}`, 64);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const cacheRoot = options.cacheRoot ?? '.pdpp';
|
|
128
|
+
const cacheFile = getCredentialCacheFile(cacheRoot, normalizedProviderUrl);
|
|
129
|
+
let payload;
|
|
130
|
+
try {
|
|
131
|
+
payload = JSON.parse(await readFile(cacheFile, 'utf8'));
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (error?.code === 'ENOENT') {
|
|
134
|
+
throw new ConnectError(
|
|
135
|
+
'not_connected',
|
|
136
|
+
`No PDPP credential found for ${normalizedProviderUrl}. Run pdpp connect ${normalizedProviderUrl} first.`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const credential = payload?.credential;
|
|
143
|
+
if (!credential?.access_token) {
|
|
144
|
+
throw new ConnectError('credential_invalid', `Credential cache entry is missing an access token: ${cacheFile}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (credential.expires_at) {
|
|
148
|
+
const expiresAtMs = Date.parse(credential.expires_at);
|
|
149
|
+
const now = options.now?.() ?? Date.now();
|
|
150
|
+
if (Number.isFinite(expiresAtMs) && expiresAtMs <= now) {
|
|
151
|
+
throw new ConnectError(
|
|
152
|
+
'credential_expired',
|
|
153
|
+
`Credential for ${normalizedProviderUrl} expired. Run pdpp connect ${normalizedProviderUrl} again.`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { cacheFile, payload, credential, providerUrl: normalizedProviderUrl };
|
|
159
|
+
}
|
|
160
|
+
|
|
106
161
|
export function normalizeProviderUrl(value) {
|
|
107
162
|
try {
|
|
108
163
|
const parsed = new URL(value.includes('://') ? value : `https://${value}`);
|
|
@@ -167,6 +222,56 @@ function findAgentConnectEndpoint(resourceMetadata, authorizationMetadata) {
|
|
|
167
222
|
}
|
|
168
223
|
}
|
|
169
224
|
|
|
225
|
+
function findRegistrationEndpoint(authorizationMetadata) {
|
|
226
|
+
const endpoint = authorizationMetadata.registration_endpoint;
|
|
227
|
+
if (!endpoint) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
const modes = authorizationMetadata.pdpp_registration_modes_supported;
|
|
231
|
+
if (Array.isArray(modes) && !modes.includes('dynamic')) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
return new URL(endpoint, authorizationMetadata.issuer).toString();
|
|
236
|
+
} catch {
|
|
237
|
+
throw new ConnectError('metadata_failure', 'Registration endpoint in provider metadata is not a valid URL.');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function getOrRegisterPublicClient({ fetchFn, authorizationMetadata, cacheRoot, providerUrl, clientName }) {
|
|
242
|
+
const cached = await readCachedClientRegistration(cacheRoot, providerUrl);
|
|
243
|
+
if (cached) {
|
|
244
|
+
return cached;
|
|
245
|
+
}
|
|
246
|
+
const registrationEndpoint = findRegistrationEndpoint(authorizationMetadata);
|
|
247
|
+
if (!registrationEndpoint) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
const registered = await postJson(fetchFn, registrationEndpoint, {
|
|
251
|
+
client_name: clientName,
|
|
252
|
+
token_endpoint_auth_method: 'none',
|
|
253
|
+
});
|
|
254
|
+
if (!registered?.client_id) {
|
|
255
|
+
throw new ConnectError('connect_contract_invalid', 'Dynamic client registration response did not include client_id.');
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
client_id: registered.client_id,
|
|
259
|
+
client_name: registered.client_name ?? clientName,
|
|
260
|
+
token_endpoint_auth_method: registered.token_endpoint_auth_method ?? 'none',
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function readCachedClientRegistration(cacheRoot, providerUrl) {
|
|
265
|
+
try {
|
|
266
|
+
const payload = JSON.parse(await readFile(getCredentialCacheFile(cacheRoot, providerUrl), 'utf8'));
|
|
267
|
+
const client = payload?.client;
|
|
268
|
+
return client?.client_id ? client : null;
|
|
269
|
+
} catch (error) {
|
|
270
|
+
if (error?.code === 'ENOENT') return null;
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
170
275
|
async function pollForCredential(fetchFn, pollUrl, options) {
|
|
171
276
|
const startedAt = options.now?.() ?? Date.now();
|
|
172
277
|
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
@@ -194,6 +299,7 @@ async function pollForCredential(fetchFn, pollUrl, options) {
|
|
|
194
299
|
access_token: credential.access_token,
|
|
195
300
|
token_type: credential.token_type ?? 'Bearer',
|
|
196
301
|
expires_at: credential.expires_at,
|
|
302
|
+
grant_id: credential.grant_id ?? result.grant_id,
|
|
197
303
|
scope: credential.scope,
|
|
198
304
|
};
|
|
199
305
|
}
|
|
@@ -238,14 +344,18 @@ async function verifySchema(fetchFn, providerUrl, accessToken) {
|
|
|
238
344
|
}
|
|
239
345
|
|
|
240
346
|
async function storeCredential(cacheRoot, providerUrl, payload) {
|
|
241
|
-
const
|
|
242
|
-
const cacheFile = join(cacheRoot, 'clients', `${host}.json`);
|
|
347
|
+
const cacheFile = getCredentialCacheFile(cacheRoot, providerUrl);
|
|
243
348
|
await mkdir(dirname(cacheFile), { recursive: true, mode: 0o700 });
|
|
244
349
|
await ensurePdppGitignore(cacheRoot);
|
|
245
350
|
await writeFile(cacheFile, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
|
|
246
351
|
return cacheFile;
|
|
247
352
|
}
|
|
248
353
|
|
|
354
|
+
function getCredentialCacheFile(cacheRoot, providerUrl) {
|
|
355
|
+
const host = new URL(providerUrl).host.replace(/[^a-zA-Z0-9.-]/g, '_');
|
|
356
|
+
return join(cacheRoot, 'clients', `${host}.json`);
|
|
357
|
+
}
|
|
358
|
+
|
|
249
359
|
async function ensurePdppGitignore(cacheRoot) {
|
|
250
360
|
const gitignorePath = join(cacheRoot, '.gitignore');
|
|
251
361
|
let current = '';
|