prized 0.0.1 → 0.18.0-rc.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/bin/prized.js +22 -7
- package/lib/install.js +112 -0
- package/lib/minisign.js +44 -0
- package/lib/release.js +36 -0
- package/package.json +10 -7
package/README.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# prized
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
with your own coding agent (Claude Code, Windsurf, Cursor, …), then deploy them
|
|
5
|
-
behind your company's SSO in seconds.
|
|
3
|
+
A cloud dev box for your coding agents.
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
```bash
|
|
6
|
+
npx prized
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Run it in the folder you want on your box. It signs you up with GitHub or Google, starts a box on the free credit, and sets up this machine: the Prized CLI installed and signed in, the folder synced, and Claude desktop, the ChatGPT app, Cursor or your terminal set up to work on the box.
|
|
10
|
+
|
|
11
|
+
Docs: https://prized.dev/docs
|
package/bin/prized.js
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
// `npx prized`: runs this package's version of the signed Prized CLI,
|
|
3
|
+
// fetched and verified on first use (lib/install.js). With no arguments it
|
|
4
|
+
// is `prized start`: sign up, get a box, set up this machine
|
|
5
|
+
// (docs/plans/npx-signup.md). Any arguments go to the CLI as they are.
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { ensureCli } from '../lib/install.js';
|
|
9
|
+
import { isReleaseVersion } from '../lib/release.js';
|
|
4
10
|
|
|
5
|
-
|
|
6
|
-
your own coding agent, then deploy them behind your company's SSO.
|
|
11
|
+
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
try {
|
|
14
|
+
if (!isReleaseVersion(version)) {
|
|
15
|
+
throw new Error(`this is a development copy (${version}); run a published version: npx prized`);
|
|
16
|
+
}
|
|
17
|
+
const bin = await ensureCli(version, { log: (line) => process.stderr.write(`${line}\n`) });
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
const run = spawnSync(bin, args.length > 0 ? args : ['start'], { stdio: 'inherit' });
|
|
20
|
+
if (run.error) throw run.error;
|
|
21
|
+
process.exit(run.status ?? 1);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
process.stderr.write(`prized: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
package/lib/install.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Fetch this package's own version of the signed Prized CLI, verify it, and
|
|
2
|
+
// keep it in a per-version cache (docs/plans/npx-signup.md §5). Nothing runs
|
|
3
|
+
// unless SHA256SUMS carries the release key's signature, its trusted comment
|
|
4
|
+
// names exactly this version, and the archive matches its line.
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { verifyMinisign } from './minisign.js';
|
|
11
|
+
import { assetFor, binaryName, cacheRoot, RELEASE_PUBKEY, RELEASES_BASE } from './release.js';
|
|
12
|
+
|
|
13
|
+
const VERIFIED = '.verified';
|
|
14
|
+
|
|
15
|
+
/** The verified CLI for `version`, downloading it the first time. */
|
|
16
|
+
export async function ensureCli(version, opts = {}) {
|
|
17
|
+
const {
|
|
18
|
+
base = RELEASES_BASE,
|
|
19
|
+
pubkey = RELEASE_PUBKEY,
|
|
20
|
+
platform = process.platform,
|
|
21
|
+
arch = process.arch,
|
|
22
|
+
root = cacheRoot(),
|
|
23
|
+
fetchImpl = fetch,
|
|
24
|
+
log = () => {},
|
|
25
|
+
} = opts;
|
|
26
|
+
const dir = join(root, version);
|
|
27
|
+
const bin = join(dir, binaryName(platform));
|
|
28
|
+
if (isVerified(dir, bin)) return bin;
|
|
29
|
+
|
|
30
|
+
const asset = assetFor(version, platform, arch);
|
|
31
|
+
const from = `${base}/cli/v${version}`;
|
|
32
|
+
log(`Downloading Prized ${version}…`);
|
|
33
|
+
const [sums, sig, archive] = await Promise.all([
|
|
34
|
+
get(fetchImpl, `${from}/SHA256SUMS`),
|
|
35
|
+
get(fetchImpl, `${from}/SHA256SUMS.minisig`),
|
|
36
|
+
get(fetchImpl, `${from}/${asset}`),
|
|
37
|
+
]);
|
|
38
|
+
const trusted = verifyMinisign(sums, sig, pubkey);
|
|
39
|
+
if (trusted !== `prized ${version}`) {
|
|
40
|
+
throw new Error(`the release's signature names "${trusted}", not prized ${version}`);
|
|
41
|
+
}
|
|
42
|
+
const want = sumFor(sums.toString('utf8'), asset);
|
|
43
|
+
const got = createHash('sha256').update(archive).digest('hex');
|
|
44
|
+
if (want === null || want !== got) throw new Error(`${asset} does not match the signed SHA256SUMS`);
|
|
45
|
+
|
|
46
|
+
// Unpacked next to the cache and renamed in, so a half-written directory
|
|
47
|
+
// is never taken for a verified one.
|
|
48
|
+
await mkdir(root, { recursive: true });
|
|
49
|
+
const tmp = join(root, `.${version}.${randomBytes(6).toString('hex')}`);
|
|
50
|
+
await mkdir(tmp, { recursive: true });
|
|
51
|
+
try {
|
|
52
|
+
const file = join(tmp, asset);
|
|
53
|
+
await writeFile(file, archive, { mode: 0o600 });
|
|
54
|
+
const tar = spawnSync('tar', [asset.endsWith('.zip') ? '-xf' : '-xzf', file, '-C', tmp], { stdio: 'pipe' });
|
|
55
|
+
if (tar.status !== 0) {
|
|
56
|
+
throw new Error(`could not unpack ${asset}: ${(tar.stderr ?? '').toString().trim() || tar.error?.message || 'tar failed'}`);
|
|
57
|
+
}
|
|
58
|
+
await rm(file);
|
|
59
|
+
if (!existsSync(join(tmp, binaryName(platform)))) throw new Error(`${asset} has no ${binaryName(platform)}`);
|
|
60
|
+
await writeFile(join(tmp, VERIFIED), `${version}\n`);
|
|
61
|
+
await moveInto(tmp, dir, bin);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
await rm(tmp, { recursive: true, force: true });
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
return bin;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isVerified(dir, bin) {
|
|
70
|
+
return existsSync(join(dir, VERIFIED)) && existsSync(bin);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Puts the unpacked tmp in place as dir. Another `npx prized` may have got
|
|
75
|
+
* there first, and may be running from it: its verified copy is the same
|
|
76
|
+
* signed release, so it stays and tmp goes. Only a copy that lost its binary
|
|
77
|
+
* (a cache cleaner) is moved aside and replaced.
|
|
78
|
+
*/
|
|
79
|
+
async function moveInto(tmp, dir, bin) {
|
|
80
|
+
for (let attempt = 0; ; attempt++) {
|
|
81
|
+
try {
|
|
82
|
+
await rename(tmp, dir);
|
|
83
|
+
return;
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (!['EEXIST', 'ENOTEMPTY', 'EPERM', 'EACCES', 'EBUSY'].includes(err.code) || attempt > 0) {
|
|
86
|
+
if (isVerified(dir, bin)) break;
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (isVerified(dir, bin)) break;
|
|
91
|
+
const stale = `${dir}.stale-${randomBytes(6).toString('hex')}`;
|
|
92
|
+
await rename(dir, stale).catch(() => {}); // a racer may have moved it already
|
|
93
|
+
await rm(stale, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
await rm(tmp, { recursive: true, force: true });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The sha256 SHA256SUMS lists for `name`, or null. */
|
|
99
|
+
export function sumFor(sums, name) {
|
|
100
|
+
for (const line of sums.split('\n')) {
|
|
101
|
+
const m = /^([0-9a-f]{64}) [ *](.+)$/.exec(line.trim());
|
|
102
|
+
if (m && m[2] === name) return m[1];
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function get(fetchImpl, url) {
|
|
108
|
+
const res = await fetchImpl(url, { redirect: 'follow' });
|
|
109
|
+
if (!res.ok) throw new Error(`${url} answered ${res.status}`);
|
|
110
|
+
return Buffer.from(await res.arrayBuffer());
|
|
111
|
+
}
|
|
112
|
+
|
package/lib/minisign.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// minisign signature verification (https://jedisct1.github.io/minisign/),
|
|
2
|
+
// Node's own crypto only: Ed25519 over the file (legacy "Ed") or over its
|
|
3
|
+
// BLAKE2b-512 (prehashed "ED"), then the global signature over the
|
|
4
|
+
// signature and the trusted comment. Returns the trusted comment; throws on
|
|
5
|
+
// anything else.
|
|
6
|
+
import { createHash, createPublicKey, verify } from 'node:crypto';
|
|
7
|
+
|
|
8
|
+
// An Ed25519 SubjectPublicKeyInfo is this prefix and the 32 raw key bytes.
|
|
9
|
+
const SPKI_ED25519 = Buffer.from('302a300506032b6570032100', 'hex');
|
|
10
|
+
|
|
11
|
+
export function parsePublicKey(b64) {
|
|
12
|
+
const raw = Buffer.from(b64.trim(), 'base64');
|
|
13
|
+
if (raw.length !== 42 || raw.subarray(0, 2).toString('latin1') !== 'Ed') {
|
|
14
|
+
throw new Error('not a minisign public key');
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
keyId: raw.subarray(2, 10),
|
|
18
|
+
key: createPublicKey({ key: Buffer.concat([SPKI_ED25519, raw.subarray(10)]), format: 'der', type: 'spki' }),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function verifyMinisign(data, signatureFile, publicKeyB64) {
|
|
23
|
+
const pub = parsePublicKey(publicKeyB64);
|
|
24
|
+
const lines = signatureFile.toString('utf8').replace(/\r\n/g, '\n').split('\n');
|
|
25
|
+
if (!lines[0]?.startsWith('untrusted comment:') || !lines[2]?.startsWith('trusted comment: ')) {
|
|
26
|
+
throw new Error('not a minisign signature');
|
|
27
|
+
}
|
|
28
|
+
const sig = Buffer.from(lines[1] ?? '', 'base64');
|
|
29
|
+
const global = Buffer.from(lines[3] ?? '', 'base64');
|
|
30
|
+
if (sig.length !== 74 || global.length !== 64) throw new Error('not a minisign signature');
|
|
31
|
+
const alg = sig.subarray(0, 2).toString('latin1');
|
|
32
|
+
if (!sig.subarray(2, 10).equals(pub.keyId)) throw new Error('signed with another key');
|
|
33
|
+
let message;
|
|
34
|
+
if (alg === 'ED') message = createHash('blake2b512').update(data).digest();
|
|
35
|
+
else if (alg === 'Ed') message = data;
|
|
36
|
+
else throw new Error('unknown minisign algorithm');
|
|
37
|
+
const signature = sig.subarray(10);
|
|
38
|
+
if (!verify(null, message, pub.key, signature)) throw new Error('the signature does not match');
|
|
39
|
+
const trusted = lines[2].slice('trusted comment: '.length);
|
|
40
|
+
if (!verify(null, Buffer.concat([signature, Buffer.from(trusted, 'utf8')]), pub.key, global)) {
|
|
41
|
+
throw new Error('the trusted comment does not match its signature');
|
|
42
|
+
}
|
|
43
|
+
return trusted;
|
|
44
|
+
}
|
package/lib/release.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Where the signed Prized CLI lives and how to name this machine's build
|
|
2
|
+
// (the release workflow's archive names, docs/plans/npx-signup.md §5).
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export const RELEASES_BASE = 'https://doppel-releases-072456928225-usw2.s3.us-west-2.amazonaws.com';
|
|
7
|
+
|
|
8
|
+
// The release key (minisign), the same one internal/release/pubkey.go
|
|
9
|
+
// builds into the CLI; test/key.test.js keeps the two equal.
|
|
10
|
+
export const RELEASE_PUBKEY = 'RWRIbc5tPnYdNEgRUmAmeIXR3z908C9Kn4ln7J0wiTBCjoSWMgTfvimR';
|
|
11
|
+
|
|
12
|
+
const GOARCH = { x64: 'amd64', arm64: 'arm64' };
|
|
13
|
+
|
|
14
|
+
/** The archive this platform installs for `version` (no leading v). */
|
|
15
|
+
export function assetFor(version, platform = process.platform, arch = process.arch) {
|
|
16
|
+
if (platform === 'darwin') return `prized_${version}_darwin_all.tar.gz`;
|
|
17
|
+
const goarch = GOARCH[arch];
|
|
18
|
+
if (goarch && platform === 'linux') return `prized_${version}_linux_${goarch}.tar.gz`;
|
|
19
|
+
if (goarch && platform === 'win32') return `prized_${version}_windows_${goarch}.zip`;
|
|
20
|
+
throw new Error(`there is no Prized CLI for ${platform}/${arch}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function binaryName(platform = process.platform) {
|
|
24
|
+
return platform === 'win32' ? 'prized.exe' : 'prized';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The per-version cache the verified CLI is kept in. */
|
|
28
|
+
export function cacheRoot(env = process.env, platform = process.platform, home = homedir()) {
|
|
29
|
+
if (platform === 'win32') return join(env.LOCALAPPDATA || join(home, 'AppData', 'Local'), 'Prized', 'npm');
|
|
30
|
+
return join(env.XDG_CACHE_HOME || join(home, '.cache'), 'prized', 'npm');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A release version this package can run: x.y.z, optionally -pre. */
|
|
34
|
+
export function isReleaseVersion(v) {
|
|
35
|
+
return /^\d+\.\d+\.\d+(-[0-9A-Za-z.]+)?$/.test(v) && !v.startsWith('0.0.0');
|
|
36
|
+
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prized",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Prized
|
|
3
|
+
"version": "0.18.0-rc.2",
|
|
4
|
+
"description": "Prized: a cloud dev box for your coding agents. npx prized signs you up, starts your box and sets up this machine.",
|
|
5
|
+
"homepage": "https://prized.dev",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"type": "module",
|
|
5
8
|
"bin": {
|
|
6
9
|
"prized": "bin/prized.js"
|
|
7
10
|
},
|
|
8
|
-
"files": [
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"lib",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
13
16
|
"engines": {
|
|
14
17
|
"node": ">=18"
|
|
15
18
|
}
|