@promigence/sdk 0.1.0-beta.12
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/NOTICE +14 -0
- package/README.md +54 -0
- package/dist/blake3.d.ts +10 -0
- package/dist/blake3.js +111 -0
- package/dist/bundle.d.ts +96 -0
- package/dist/bundle.js +227 -0
- package/dist/ca-guard.d.ts +1 -0
- package/dist/ca-guard.js +166 -0
- package/dist/caller.d.ts +73 -0
- package/dist/caller.js +47 -0
- package/dist/cjs/blake3.d.ts +10 -0
- package/dist/cjs/blake3.js +116 -0
- package/dist/cjs/bundle.d.ts +96 -0
- package/dist/cjs/bundle.js +242 -0
- package/dist/cjs/ca-guard.d.ts +1 -0
- package/dist/cjs/ca-guard.js +174 -0
- package/dist/cjs/caller.d.ts +73 -0
- package/dist/cjs/caller.js +57 -0
- package/dist/cjs/client.d.ts +3015 -0
- package/dist/cjs/client.js +2617 -0
- package/dist/cjs/command-errors.d.ts +41 -0
- package/dist/cjs/command-errors.js +43 -0
- package/dist/cjs/context.d.ts +119 -0
- package/dist/cjs/context.js +598 -0
- package/dist/cjs/core.d.ts +285 -0
- package/dist/cjs/core.js +290 -0
- package/dist/cjs/credstore.d.ts +90 -0
- package/dist/cjs/credstore.js +219 -0
- package/dist/cjs/dispatcher.d.ts +58 -0
- package/dist/cjs/dispatcher.js +91 -0
- package/dist/cjs/github.d.ts +158 -0
- package/dist/cjs/github.js +84 -0
- package/dist/cjs/gitlab.d.ts +104 -0
- package/dist/cjs/gitlab.js +24 -0
- package/dist/cjs/handles.d.ts +82 -0
- package/dist/cjs/handles.js +139 -0
- package/dist/cjs/hint.d.ts +1 -0
- package/dist/cjs/hint.js +10 -0
- package/dist/cjs/http2.d.ts +206 -0
- package/dist/cjs/http2.js +1932 -0
- package/dist/cjs/index.d.ts +34 -0
- package/dist/cjs/index.js +31 -0
- package/dist/cjs/keyed.gen.d.ts +1 -0
- package/dist/cjs/keyed.gen.js +10 -0
- package/dist/cjs/models.gen.d.ts +6162 -0
- package/dist/cjs/models.gen.js +2 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/proxy.d.ts +40 -0
- package/dist/cjs/proxy.js +257 -0
- package/dist/cjs/pty.d.ts +171 -0
- package/dist/cjs/pty.js +504 -0
- package/dist/cjs/sandbox.d.ts +537 -0
- package/dist/cjs/sandbox.js +736 -0
- package/dist/cjs/ssh.d.ts +25 -0
- package/dist/cjs/ssh.js +117 -0
- package/dist/cjs/trace.d.ts +33 -0
- package/dist/cjs/trace.js +88 -0
- package/dist/cjs/transport-worker.d.ts +1 -0
- package/dist/cjs/transport-worker.js +468 -0
- package/dist/cjs/warm.d.ts +1 -0
- package/dist/cjs/warm.js +43 -0
- package/dist/client.d.ts +3015 -0
- package/dist/client.js +2583 -0
- package/dist/command-errors.d.ts +41 -0
- package/dist/command-errors.js +36 -0
- package/dist/context.d.ts +119 -0
- package/dist/context.js +588 -0
- package/dist/core.d.ts +285 -0
- package/dist/core.js +266 -0
- package/dist/credstore.d.ts +90 -0
- package/dist/credstore.js +206 -0
- package/dist/dispatcher.d.ts +58 -0
- package/dist/dispatcher.js +86 -0
- package/dist/github.d.ts +158 -0
- package/dist/github.js +78 -0
- package/dist/gitlab.d.ts +104 -0
- package/dist/gitlab.js +20 -0
- package/dist/handles.d.ts +82 -0
- package/dist/handles.js +135 -0
- package/dist/hint.d.ts +1 -0
- package/dist/hint.js +7 -0
- package/dist/http2.d.ts +206 -0
- package/dist/http2.js +1921 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +15 -0
- package/dist/keyed.gen.d.ts +1 -0
- package/dist/keyed.gen.js +7 -0
- package/dist/models.gen.d.ts +6162 -0
- package/dist/models.gen.js +1 -0
- package/dist/proxy.d.ts +40 -0
- package/dist/proxy.js +249 -0
- package/dist/pty.d.ts +171 -0
- package/dist/pty.js +496 -0
- package/dist/sandbox.d.ts +537 -0
- package/dist/sandbox.js +725 -0
- package/dist/ssh.d.ts +25 -0
- package/dist/ssh.js +114 -0
- package/dist/trace.d.ts +33 -0
- package/dist/trace.js +80 -0
- package/dist/transport-worker.d.ts +1 -0
- package/dist/transport-worker.js +466 -0
- package/dist/warm.d.ts +1 -0
- package/dist/warm.js +40 -0
- package/package.json +54 -0
package/NOTICE
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Promigence — SDK and CLI (npm package `promigence`)
|
|
2
|
+
Copyright 2026 Promigence AI
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# @promigence/sdk
|
|
2
|
+
|
|
3
|
+
The fastest sandboxes for AI agents and code. The TypeScript SDK for Promigence: snapshot an environment once,
|
|
4
|
+
fork it as often as you need, run commands in every copy. Zero runtime dependencies; `import` or
|
|
5
|
+
`require()` on Node.js 20 or newer.
|
|
6
|
+
|
|
7
|
+
Docs: https://www.promigence.ai/docs
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm i @promigence/sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
You need a Promigence account (private beta, by invite). The quickest way to create one is the CLI:
|
|
16
|
+
`npm i -g promigence`, then `promigence signup`. The SDK uses the key you pass (`apiKey`) or
|
|
17
|
+
`PROMIGENCE_API_KEY`; with neither, the key the CLI stored: the system keychain, or
|
|
18
|
+
`~/.config/promigence/credentials`. A key you pass or set is used as is; the stored one is not read.
|
|
19
|
+
|
|
20
|
+
## Use it
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { Sandbox } from "@promigence/sdk";
|
|
24
|
+
|
|
25
|
+
const sbx = await Sandbox.create();
|
|
26
|
+
const r = await sbx.commands.run("python3 -c 'print(6*7)'");
|
|
27
|
+
console.log(r.stdout); // 42
|
|
28
|
+
await sbx.kill();
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Creating a client ahead of time? Call `client.preconnect()` right after: it starts opening the connection so
|
|
32
|
+
your first request does not wait for it. It never throws and sends nothing.
|
|
33
|
+
|
|
34
|
+
When `PROMIGENCE_API_KEY` is set in the environment, the SDK starts connecting as soon as it loads, so your
|
|
35
|
+
first request does not wait for the connection; nothing is sent until you make a request. Set
|
|
36
|
+
`PROMIGENCE_PRECONNECT=0` to turn this off.
|
|
37
|
+
|
|
38
|
+
Two surfaces:
|
|
39
|
+
|
|
40
|
+
- `Sandbox`: a simple class for one sandbox (`create`, `connect`, `commands.run`, `files`, `pty`,
|
|
41
|
+
`fork`, `kill`).
|
|
42
|
+
- `PromigenceClient`: the native client (`runs.*`, `sandboxes.*`, `snapshots.*`, `me()`), with a quote
|
|
43
|
+
before a run and a hard spend cap.
|
|
44
|
+
|
|
45
|
+
Errors are typed (`PromigenceError` and its subclasses), and each carries a `code`, a `fix` and a
|
|
46
|
+
link to its documentation.
|
|
47
|
+
|
|
48
|
+
## Support
|
|
49
|
+
|
|
50
|
+
support@promigence.ai
|
|
51
|
+
|
|
52
|
+
## Licence
|
|
53
|
+
|
|
54
|
+
Apache-2.0. See `NOTICE`.
|
package/dist/blake3.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BLAKE3, hand-rolled.
|
|
3
|
+
*
|
|
4
|
+
* Scope: unkeyed hashing, 32-byte output. No keyed mode, no derive-key, no XOF — nothing in the
|
|
5
|
+
* bundle format needs them.
|
|
6
|
+
*/
|
|
7
|
+
/** 32-byte digest, lowercase hex. */
|
|
8
|
+
export declare function blake3Hex(input: Uint8Array): string;
|
|
9
|
+
/** `blake3:<hex>` of a UTF-8 string — the digest form the manifest uses everywhere. */
|
|
10
|
+
export declare const blake3Ref: (text: string) => string;
|
package/dist/blake3.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const IV = new Uint32Array([
|
|
2
|
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
|
3
|
+
]);
|
|
4
|
+
const PERM = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8];
|
|
5
|
+
const CHUNK_START = 1, CHUNK_END = 2, PARENT = 4, ROOT = 8;
|
|
6
|
+
const BLOCK_LEN = 64, CHUNK_LEN = 1024;
|
|
7
|
+
const rotr = (x, n) => ((x >>> n) | (x << (32 - n))) >>> 0;
|
|
8
|
+
function g(s, a, b, c, d, mx, my) {
|
|
9
|
+
s[a] = (s[a] + s[b] + mx) >>> 0;
|
|
10
|
+
s[d] = rotr(s[d] ^ s[a], 16);
|
|
11
|
+
s[c] = (s[c] + s[d]) >>> 0;
|
|
12
|
+
s[b] = rotr(s[b] ^ s[c], 12);
|
|
13
|
+
s[a] = (s[a] + s[b] + my) >>> 0;
|
|
14
|
+
s[d] = rotr(s[d] ^ s[a], 8);
|
|
15
|
+
s[c] = (s[c] + s[d]) >>> 0;
|
|
16
|
+
s[b] = rotr(s[b] ^ s[c], 7);
|
|
17
|
+
}
|
|
18
|
+
function compress(cv, m, counter, blockLen, flags) {
|
|
19
|
+
const s = new Uint32Array(16);
|
|
20
|
+
for (let i = 0; i < 8; i++)
|
|
21
|
+
s[i] = cv[i];
|
|
22
|
+
for (let i = 0; i < 4; i++)
|
|
23
|
+
s[8 + i] = IV[i];
|
|
24
|
+
s[12] = counter >>> 0;
|
|
25
|
+
s[13] = Math.floor(counter / 0x100000000) >>> 0;
|
|
26
|
+
s[14] = blockLen >>> 0;
|
|
27
|
+
s[15] = flags >>> 0;
|
|
28
|
+
let x = m;
|
|
29
|
+
for (let r = 0; r < 7; r++) {
|
|
30
|
+
g(s, 0, 4, 8, 12, x[0], x[1]);
|
|
31
|
+
g(s, 1, 5, 9, 13, x[2], x[3]);
|
|
32
|
+
g(s, 2, 6, 10, 14, x[4], x[5]);
|
|
33
|
+
g(s, 3, 7, 11, 15, x[6], x[7]);
|
|
34
|
+
g(s, 0, 5, 10, 15, x[8], x[9]);
|
|
35
|
+
g(s, 1, 6, 11, 12, x[10], x[11]);
|
|
36
|
+
g(s, 2, 7, 8, 13, x[12], x[13]);
|
|
37
|
+
g(s, 3, 4, 9, 14, x[14], x[15]);
|
|
38
|
+
if (r < 6) {
|
|
39
|
+
const y = new Uint32Array(16);
|
|
40
|
+
for (let i = 0; i < 16; i++)
|
|
41
|
+
y[i] = x[PERM[i]];
|
|
42
|
+
x = y;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
for (let i = 0; i < 8; i++) {
|
|
46
|
+
s[i] = (s[i] ^ s[i + 8]) >>> 0;
|
|
47
|
+
s[i + 8] = (s[i + 8] ^ cv[i]) >>> 0;
|
|
48
|
+
}
|
|
49
|
+
return s;
|
|
50
|
+
}
|
|
51
|
+
function words(b, off, len) {
|
|
52
|
+
const m = new Uint32Array(16);
|
|
53
|
+
for (let i = 0; i < len; i++)
|
|
54
|
+
m[i >> 2] |= b[off + i] << ((i & 3) * 8);
|
|
55
|
+
for (let i = 0; i < 16; i++)
|
|
56
|
+
m[i] = m[i] >>> 0;
|
|
57
|
+
return m;
|
|
58
|
+
}
|
|
59
|
+
const chainingValue = (o) => compress(o.cv, o.m, o.counter, o.blockLen, o.flags).slice(0, 8);
|
|
60
|
+
function chunkOutput(b, off, len, counter) {
|
|
61
|
+
let cv = IV;
|
|
62
|
+
const blocks = Math.max(1, Math.ceil(len / BLOCK_LEN));
|
|
63
|
+
for (let i = 0; i < blocks - 1; i++) {
|
|
64
|
+
const flags = i === 0 ? CHUNK_START : 0;
|
|
65
|
+
cv = compress(cv, words(b, off + i * BLOCK_LEN, BLOCK_LEN), counter, BLOCK_LEN, flags).slice(0, 8);
|
|
66
|
+
}
|
|
67
|
+
const last = (blocks - 1) * BLOCK_LEN;
|
|
68
|
+
return {
|
|
69
|
+
cv,
|
|
70
|
+
m: words(b, off + last, len - last),
|
|
71
|
+
counter,
|
|
72
|
+
blockLen: len - last,
|
|
73
|
+
flags: (blocks === 1 ? CHUNK_START : 0) | CHUNK_END,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function parentOutput(l, r) {
|
|
77
|
+
const m = new Uint32Array(16);
|
|
78
|
+
m.set(l, 0);
|
|
79
|
+
m.set(r, 8);
|
|
80
|
+
return { cv: IV, m, counter: 0, blockLen: BLOCK_LEN, flags: PARENT };
|
|
81
|
+
}
|
|
82
|
+
function leftLen(n) {
|
|
83
|
+
let l = CHUNK_LEN;
|
|
84
|
+
while (l * 2 < n)
|
|
85
|
+
l *= 2;
|
|
86
|
+
return l;
|
|
87
|
+
}
|
|
88
|
+
function subtree(b, off, len, counter) {
|
|
89
|
+
if (len <= CHUNK_LEN)
|
|
90
|
+
return chainingValue(chunkOutput(b, off, len, counter));
|
|
91
|
+
const ll = leftLen(len);
|
|
92
|
+
return chainingValue(parentOutput(subtree(b, off, ll, counter), subtree(b, off + ll, len - ll, counter + ll / CHUNK_LEN)));
|
|
93
|
+
}
|
|
94
|
+
export function blake3Hex(input) {
|
|
95
|
+
const n = input.length;
|
|
96
|
+
const root = n <= CHUNK_LEN
|
|
97
|
+
? chunkOutput(input, 0, n, 0)
|
|
98
|
+
: (() => {
|
|
99
|
+
const ll = leftLen(n);
|
|
100
|
+
return parentOutput(subtree(input, 0, ll, 0), subtree(input, ll, n - ll, ll / CHUNK_LEN));
|
|
101
|
+
})();
|
|
102
|
+
const s = compress(root.cv, root.m, 0, root.blockLen, root.flags | ROOT);
|
|
103
|
+
let out = "";
|
|
104
|
+
for (let i = 0; i < 8; i++) {
|
|
105
|
+
const w = s[i];
|
|
106
|
+
for (let k = 0; k < 4; k++)
|
|
107
|
+
out += (((w >>> (k * 8)) & 0xff) | 0x100).toString(16).slice(1);
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
export const blake3Ref = (text) => `blake3:${blake3Hex(new TextEncoder().encode(text))}`;
|
package/dist/bundle.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export declare const BUNDLE_SCHEMA: "promigence.bundle/v1";
|
|
2
|
+
export declare const MANIFEST_SCHEMA: "promigence.snapshot/v1";
|
|
3
|
+
export declare const BUNDLE_EXT = ".promigence";
|
|
4
|
+
export type Json = null | boolean | number | string | Json[] | {
|
|
5
|
+
[k: string]: Json;
|
|
6
|
+
};
|
|
7
|
+
/** Section digests: `provenance` and `verification` always; a full or citation bundle records one per section it has. */
|
|
8
|
+
export interface Digests {
|
|
9
|
+
provenance: string;
|
|
10
|
+
verification: string;
|
|
11
|
+
[section: string]: string;
|
|
12
|
+
}
|
|
13
|
+
/** The sealed sections of a bundle: the digests of the sections the API did not disclose. */
|
|
14
|
+
export interface Sealed {
|
|
15
|
+
platform: string;
|
|
16
|
+
machine: string;
|
|
17
|
+
layers: string;
|
|
18
|
+
}
|
|
19
|
+
/** On a citation bundle: the digest of the one section it leaves out. */
|
|
20
|
+
export interface LayersRef {
|
|
21
|
+
digest: string;
|
|
22
|
+
[extra: string]: unknown;
|
|
23
|
+
}
|
|
24
|
+
export interface Bundle {
|
|
25
|
+
schema: string;
|
|
26
|
+
snap_id: string;
|
|
27
|
+
manifest_hash: string;
|
|
28
|
+
alias?: string;
|
|
29
|
+
digests: Digests;
|
|
30
|
+
/** Empty on a sealed bundle. */
|
|
31
|
+
layers: Json[];
|
|
32
|
+
/** Present only on a citation bundle. */
|
|
33
|
+
layers_ref?: LayersRef;
|
|
34
|
+
/** Present only on a sealed bundle. */
|
|
35
|
+
sealed?: Sealed;
|
|
36
|
+
provenance: Record<string, Json>;
|
|
37
|
+
verification: Record<string, Json>;
|
|
38
|
+
/** A full or citation bundle's other sections, each covered by a digest. */
|
|
39
|
+
[section: string]: unknown;
|
|
40
|
+
}
|
|
41
|
+
export type Form = "full" | "citation" | "sealed";
|
|
42
|
+
/**
|
|
43
|
+
* Canonical JSON (RFC 8785): what every digest in a bundle is computed over.
|
|
44
|
+
*
|
|
45
|
+
* Two rules come free in JavaScript and are used deliberately rather than re-implemented:
|
|
46
|
+
* `<` on strings compares UTF-16 code units, which is exactly RFC 8785's key ordering; and
|
|
47
|
+
* `String(n)` *is* ECMAScript's `Number.prototype.toString`, which is exactly its number form. `JSON.stringify`
|
|
48
|
+
* of a string is its escaping (only `"` `\` and controls, short forms, lowercase `\u00xx`).
|
|
49
|
+
*/
|
|
50
|
+
export declare function canonicalize(v: unknown): string;
|
|
51
|
+
/** `blake3:<hex>` of a value's canonical JSON. */
|
|
52
|
+
export declare const digestOf: (v: unknown) => string;
|
|
53
|
+
/**
|
|
54
|
+
* Insert insignificant whitespace into canonical JSON. A whitespace transform rather than a
|
|
55
|
+
* second serializer, so key order, number form and escaping are canonical by construction and
|
|
56
|
+
* `strip` recovers the exact bytes that were hashed.
|
|
57
|
+
*/
|
|
58
|
+
export declare function pretty(canonical: string): string;
|
|
59
|
+
/** The exact inverse of `pretty`: drop every whitespace byte outside a string. */
|
|
60
|
+
export declare function strip(text: string): string;
|
|
61
|
+
export interface BuildOpts {
|
|
62
|
+
alias?: string | undefined;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Build a bundle from a snapshot's manifest as the API serves it (`snapshots.manifest`): the
|
|
66
|
+
* sealed form, which names exactly one environment. The id is the head of `manifest_hash`, which
|
|
67
|
+
* `verifyBundle` checks.
|
|
68
|
+
*/
|
|
69
|
+
export declare function buildBundle(manifest: Record<string, unknown>, opts?: BuildOpts): Bundle;
|
|
70
|
+
/** RFC 8785 canonical bytes: what every digest is computed over. */
|
|
71
|
+
export declare const canonicalBundle: (b: Bundle) => string;
|
|
72
|
+
/** The file: the canonical bytes with whitespace put back, plus a final newline. */
|
|
73
|
+
export declare const renderBundle: (b: Bundle) => string;
|
|
74
|
+
/** `sealed` (what an export writes), `citation` (one section left out, pinned by its digest) or `full`. */
|
|
75
|
+
export declare function bundleForm(b: Bundle): Form;
|
|
76
|
+
/** The digest that names the platform a sealed bundle's environment runs on; `undefined` on any other form. */
|
|
77
|
+
export declare const platformOf: (b: Bundle) => string | undefined;
|
|
78
|
+
export type BundleErrorKind = "schema" | "snap_id_format" | "hash_format" | "id_mismatch" | "section_digest" | "manifest_hash" | "canonical";
|
|
79
|
+
export declare class BundleVerifyError extends Error {
|
|
80
|
+
readonly kind: BundleErrorKind;
|
|
81
|
+
readonly detail: Record<string, unknown>;
|
|
82
|
+
constructor(kind: BundleErrorKind, message: string, detail?: Record<string, unknown>);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Everything checkable without the network, in the order that makes each failure specific: a
|
|
86
|
+
* changed section is a `section_digest` failure naming that section, a changed `manifest_hash` is
|
|
87
|
+
* a `manifest_hash` failure (a full bundle) or an `id_mismatch`, and a rewritten `snap_id` with
|
|
88
|
+
* intact content is `id_mismatch`.
|
|
89
|
+
*
|
|
90
|
+
* A sealed bundle's provenance and verification digests and its identity are verified here; the
|
|
91
|
+
* sealed sections are the API's to answer. A full or citation bundle has every section it carries
|
|
92
|
+
* checked against its recorded digest, and a full one its `manifest_hash` against its content.
|
|
93
|
+
*/
|
|
94
|
+
export declare function verifyBundle(b: Bundle): Form;
|
|
95
|
+
/** Parse a `.promigence` file. Refuses anything that is not JSON or not a bundle; `verifyBundle` does the rest. */
|
|
96
|
+
export declare function parseBundle(text: string): Bundle;
|
package/dist/bundle.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { blake3Ref } from "./blake3.js";
|
|
2
|
+
export const BUNDLE_SCHEMA = "promigence.bundle/v1";
|
|
3
|
+
export const MANIFEST_SCHEMA = "promigence.snapshot/v1";
|
|
4
|
+
export const BUNDLE_EXT = ".promigence";
|
|
5
|
+
export function canonicalize(v) {
|
|
6
|
+
if (v === null)
|
|
7
|
+
return "null";
|
|
8
|
+
switch (typeof v) {
|
|
9
|
+
case "boolean": return v ? "true" : "false";
|
|
10
|
+
case "number":
|
|
11
|
+
if (!Number.isFinite(v))
|
|
12
|
+
throw new TypeError(`non-finite number cannot be canonicalised: ${v}`);
|
|
13
|
+
return Object.is(v, -0) ? "0" : String(v);
|
|
14
|
+
case "string": return JSON.stringify(v);
|
|
15
|
+
case "object": break;
|
|
16
|
+
default: throw new TypeError(`cannot canonicalise ${typeof v}`);
|
|
17
|
+
}
|
|
18
|
+
if (Array.isArray(v))
|
|
19
|
+
return `[${v.map(canonicalize).join(",")}]`;
|
|
20
|
+
const o = v;
|
|
21
|
+
const keys = Object.keys(o).filter((k) => o[k] !== undefined).sort();
|
|
22
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(o[k])}`).join(",")}}`;
|
|
23
|
+
}
|
|
24
|
+
export const digestOf = (v) => blake3Ref(canonicalize(v));
|
|
25
|
+
export function pretty(canonical) {
|
|
26
|
+
let out = "";
|
|
27
|
+
let depth = 0, inStr = false, esc = false;
|
|
28
|
+
for (let i = 0; i < canonical.length; i++) {
|
|
29
|
+
const c = canonical[i];
|
|
30
|
+
if (inStr) {
|
|
31
|
+
out += c;
|
|
32
|
+
if (esc)
|
|
33
|
+
esc = false;
|
|
34
|
+
else if (c === "\\")
|
|
35
|
+
esc = true;
|
|
36
|
+
else if (c === '"')
|
|
37
|
+
inStr = false;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (c === '"') {
|
|
41
|
+
inStr = true;
|
|
42
|
+
out += c;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (c === "{" || c === "[") {
|
|
46
|
+
const close = c === "{" ? "}" : "]";
|
|
47
|
+
if (canonical[i + 1] === close) {
|
|
48
|
+
out += c + close;
|
|
49
|
+
i++;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
depth++;
|
|
53
|
+
out += c + "\n" + " ".repeat(depth);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (c === "}" || c === "]") {
|
|
57
|
+
depth = Math.max(0, depth - 1);
|
|
58
|
+
out += "\n" + " ".repeat(depth) + c;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (c === ",") {
|
|
62
|
+
out += ",\n" + " ".repeat(depth);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (c === ":") {
|
|
66
|
+
out += ": ";
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
out += c;
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
export function strip(text) {
|
|
74
|
+
let out = "";
|
|
75
|
+
let inStr = false, esc = false;
|
|
76
|
+
for (const c of text) {
|
|
77
|
+
if (inStr) {
|
|
78
|
+
out += c;
|
|
79
|
+
if (esc)
|
|
80
|
+
esc = false;
|
|
81
|
+
else if (c === "\\")
|
|
82
|
+
esc = true;
|
|
83
|
+
else if (c === '"')
|
|
84
|
+
inStr = false;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (c === '"') {
|
|
88
|
+
inStr = true;
|
|
89
|
+
out += c;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (c === " " || c === "\t" || c === "\n" || c === "\r")
|
|
93
|
+
continue;
|
|
94
|
+
out += c;
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
const idOf = (hash) => `snap_${hash.slice(7, 7 + 24)}`;
|
|
99
|
+
export function buildBundle(manifest, opts = {}) {
|
|
100
|
+
const sealedIn = manifest?.["sealed"];
|
|
101
|
+
if (!sealedIn || typeof sealedIn !== "object" || Array.isArray(sealedIn)) {
|
|
102
|
+
throw new BundleVerifyError("schema", "a bundle is built from a sealed manifest, and this one is not sealed");
|
|
103
|
+
}
|
|
104
|
+
const alias = opts.alias ?? manifest["alias"] ?? undefined;
|
|
105
|
+
const s = sealedIn;
|
|
106
|
+
const hash = String(manifest["manifest_hash"] ?? "");
|
|
107
|
+
return {
|
|
108
|
+
schema: BUNDLE_SCHEMA,
|
|
109
|
+
snap_id: idOf(hash),
|
|
110
|
+
manifest_hash: hash,
|
|
111
|
+
...(alias ? { alias } : {}),
|
|
112
|
+
digests: {
|
|
113
|
+
provenance: digestOf(manifest["provenance"]),
|
|
114
|
+
verification: digestOf(manifest["verification"]),
|
|
115
|
+
},
|
|
116
|
+
layers: [],
|
|
117
|
+
sealed: { platform: String(s["platform"] ?? ""), machine: String(s["machine"] ?? ""), layers: String(s["layers"] ?? "") },
|
|
118
|
+
provenance: manifest["provenance"],
|
|
119
|
+
verification: manifest["verification"],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
export const canonicalBundle = (b) => canonicalize(b);
|
|
123
|
+
export const renderBundle = (b) => pretty(canonicalize(b)) + "\n";
|
|
124
|
+
export function bundleForm(b) {
|
|
125
|
+
if (b.sealed !== undefined)
|
|
126
|
+
return "sealed";
|
|
127
|
+
return b.layers_ref !== undefined ? "citation" : "full";
|
|
128
|
+
}
|
|
129
|
+
export const platformOf = (b) => b.sealed?.platform;
|
|
130
|
+
export class BundleVerifyError extends Error {
|
|
131
|
+
kind;
|
|
132
|
+
detail;
|
|
133
|
+
constructor(kind, message, detail = {}) {
|
|
134
|
+
super(message);
|
|
135
|
+
this.name = "BundleVerifyError";
|
|
136
|
+
this.kind = kind;
|
|
137
|
+
this.detail = detail;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const HEX = (s, n) => new RegExp(`^[0-9a-f]{${n}}$`).test(s);
|
|
141
|
+
function checkHash(field, h) {
|
|
142
|
+
const s = String(h ?? "");
|
|
143
|
+
if (!s.startsWith("blake3:") || !HEX(s.slice(7), 64)) {
|
|
144
|
+
throw new BundleVerifyError("hash_format", `${field} ${JSON.stringify(s)} is not blake3:<64 lowercase hex>`, { field, found: s });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function section(name, recorded, v) {
|
|
148
|
+
const computed = digestOf(v);
|
|
149
|
+
if (computed !== recorded) {
|
|
150
|
+
throw new BundleVerifyError("section_digest", `section \`${name}\` does not match its recorded digest: recorded ${recorded}, computed ${computed}`, { section: name, recorded, computed });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function checkId(b) {
|
|
154
|
+
const derived = idOf(b.manifest_hash);
|
|
155
|
+
if (derived !== b.snap_id) {
|
|
156
|
+
throw new BundleVerifyError("id_mismatch", `snap_id ${b.snap_id} does not match manifest_hash (which names ${derived}): the identity and the content of this bundle disagree`, { snap_id: b.snap_id, derived });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
export function verifyBundle(b) {
|
|
160
|
+
if (b?.schema !== BUNDLE_SCHEMA) {
|
|
161
|
+
throw new BundleVerifyError("schema", `not a ${BUNDLE_SCHEMA} document: schema is ${JSON.stringify(b?.schema ?? null)}`, { found: b?.schema });
|
|
162
|
+
}
|
|
163
|
+
if (!/^snap_[0-9a-f]{24}$/.test(b.snap_id ?? "")) {
|
|
164
|
+
throw new BundleVerifyError("snap_id_format", `snap_id ${JSON.stringify(b.snap_id ?? null)} is not snap_<24 lowercase hex>`, { found: b.snap_id });
|
|
165
|
+
}
|
|
166
|
+
checkHash("manifest_hash", b.manifest_hash);
|
|
167
|
+
const form = bundleForm(b);
|
|
168
|
+
if (form === "sealed") {
|
|
169
|
+
for (const k of ["platform", "machine", "layers"])
|
|
170
|
+
checkHash(`sealed.${k}`, b.sealed?.[k]);
|
|
171
|
+
for (const k of ["provenance", "verification"])
|
|
172
|
+
checkHash(`digests.${k}`, b.digests?.[k]);
|
|
173
|
+
section("provenance", b.digests.provenance, b.provenance);
|
|
174
|
+
section("verification", b.digests.verification, b.verification);
|
|
175
|
+
checkId(b);
|
|
176
|
+
return form;
|
|
177
|
+
}
|
|
178
|
+
const digests = b.digests && typeof b.digests === "object" ? { ...b.digests } : {};
|
|
179
|
+
const names = Object.keys(digests).sort();
|
|
180
|
+
for (const k of ["provenance", "verification"]) {
|
|
181
|
+
if (!names.includes(k))
|
|
182
|
+
throw new BundleVerifyError("schema", `digests.${k} is missing`, { form });
|
|
183
|
+
}
|
|
184
|
+
for (const k of names)
|
|
185
|
+
checkHash(`digests.${k}`, digests[k]);
|
|
186
|
+
let omitted;
|
|
187
|
+
if (form === "citation") {
|
|
188
|
+
const pinned = String(b.layers_ref?.digest ?? "");
|
|
189
|
+
checkHash("layers_ref.digest", pinned);
|
|
190
|
+
omitted = names.find((k) => digests[k] === pinned);
|
|
191
|
+
if (omitted === undefined) {
|
|
192
|
+
throw new BundleVerifyError("section_digest", `layers_ref.digest ${pinned} pins no section this bundle records`, { recorded: pinned });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
for (const k of names) {
|
|
196
|
+
if (k === omitted)
|
|
197
|
+
continue;
|
|
198
|
+
if (b[k] === undefined)
|
|
199
|
+
throw new BundleVerifyError("schema", `section \`${k}\` is missing`, { section: k });
|
|
200
|
+
section(k, digests[k], b[k]);
|
|
201
|
+
}
|
|
202
|
+
if (form === "full") {
|
|
203
|
+
const content = { schema: MANIFEST_SCHEMA };
|
|
204
|
+
for (const k of names)
|
|
205
|
+
content[k] = b[k];
|
|
206
|
+
const computed = blake3Ref(canonicalize(content));
|
|
207
|
+
if (computed !== b.manifest_hash) {
|
|
208
|
+
throw new BundleVerifyError("manifest_hash", `manifest_hash mismatch: the bundle records ${b.manifest_hash}, its own contents hash to ${computed}`, { recorded: b.manifest_hash, computed });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
checkId(b);
|
|
212
|
+
return form;
|
|
213
|
+
}
|
|
214
|
+
export function parseBundle(text) {
|
|
215
|
+
let v;
|
|
216
|
+
try {
|
|
217
|
+
v = JSON.parse(text);
|
|
218
|
+
}
|
|
219
|
+
catch (e) {
|
|
220
|
+
throw new BundleVerifyError("canonical", `not JSON: ${e.message}`);
|
|
221
|
+
}
|
|
222
|
+
const b = v;
|
|
223
|
+
if (b?.schema !== BUNDLE_SCHEMA) {
|
|
224
|
+
throw new BundleVerifyError("schema", `not a ${BUNDLE_SCHEMA} document: schema is ${JSON.stringify(b?.schema ?? null)}`, { found: b?.schema });
|
|
225
|
+
}
|
|
226
|
+
return b;
|
|
227
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/ca-guard.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import * as tls from "node:tls";
|
|
3
|
+
const DISTRUSTED_SPKI_SHA256 = new Set([
|
|
4
|
+
"ebed6475c2c2d80fcb199f3fe8dd6e6b3ce60b77eb15d4e42c3dec0c6f5b5105",
|
|
5
|
+
"49dd353dc2b919a3770645117d0e60c16115e7d1bc259eefe734b00ea434fe5f",
|
|
6
|
+
"e0254c7fcdc265e2f0f8a428dabb75ab4ae039b4872d62f5ee57f0877d5a8a41",
|
|
7
|
+
"5c0660a2b164910d8ff589c18e14552e9e0ec237dcefe2cb736d41a4811f81a0",
|
|
8
|
+
"43bffa19d6ef651451f6841e019931e96f6f2eaeff28ab4b6f1ce4576de2717b",
|
|
9
|
+
"63ecefee431d49523bc1136c4e72d0206a477086caf606cd5cf025686c109cf8",
|
|
10
|
+
"ec4ffa1ad0ac88081314b7aaffef6a91f97cd57291bb5ba686c5415f5eb4f6b6",
|
|
11
|
+
"92c063d67e8f14db71d288e11266ffdaf69cbf115f7bab14f3bc2f68d25e0e30",
|
|
12
|
+
]);
|
|
13
|
+
const MAX_CHAIN_DEPTH = 10;
|
|
14
|
+
const COMPROMISED_KEY_MESSAGE = "The server's certificate chain uses a key that is publicly known to be compromised, so the connection was refused.";
|
|
15
|
+
export class CompromisedKeyError extends Error {
|
|
16
|
+
constructor() {
|
|
17
|
+
super(COMPROMISED_KEY_MESSAGE);
|
|
18
|
+
this.name = "CompromisedKeyError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function compromisedKeyCause(err) {
|
|
22
|
+
try {
|
|
23
|
+
let e = err;
|
|
24
|
+
for (let i = 0; i < 4; i++) {
|
|
25
|
+
if (typeof e !== "object" || e === null)
|
|
26
|
+
return undefined;
|
|
27
|
+
if (e instanceof CompromisedKeyError)
|
|
28
|
+
return e;
|
|
29
|
+
e = e.cause;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const TEST_HASHES = new Set();
|
|
37
|
+
let parseCount = 0;
|
|
38
|
+
function spkiSha256(raw) {
|
|
39
|
+
parseCount++;
|
|
40
|
+
try {
|
|
41
|
+
const der = new crypto.X509Certificate(raw).publicKey.export({ type: "spki", format: "der" });
|
|
42
|
+
return crypto.createHash("sha256").update(der).digest("hex");
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const SPKI_BY_FINGERPRINT = new Map();
|
|
49
|
+
const SPKI_CACHE_MAX = 512;
|
|
50
|
+
function certSpkiSha256(node, raw) {
|
|
51
|
+
const fp = node.fingerprint256;
|
|
52
|
+
if (typeof fp !== "string" || fp === "")
|
|
53
|
+
return spkiSha256(raw);
|
|
54
|
+
const hit = SPKI_BY_FINGERPRINT.get(fp);
|
|
55
|
+
if (hit !== undefined)
|
|
56
|
+
return hit;
|
|
57
|
+
const hash = spkiSha256(raw);
|
|
58
|
+
if (hash === undefined)
|
|
59
|
+
return undefined;
|
|
60
|
+
if (SPKI_BY_FINGERPRINT.size >= SPKI_CACHE_MAX) {
|
|
61
|
+
for (const oldest of SPKI_BY_FINGERPRINT.keys()) {
|
|
62
|
+
SPKI_BY_FINGERPRINT.delete(oldest);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
SPKI_BY_FINGERPRINT.set(fp, hash);
|
|
67
|
+
return hash;
|
|
68
|
+
}
|
|
69
|
+
export function chainHasCompromisedKey(cert) {
|
|
70
|
+
try {
|
|
71
|
+
let node = cert;
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
for (let depth = 0; node && depth < MAX_CHAIN_DEPTH; depth++) {
|
|
74
|
+
if (seen.has(node))
|
|
75
|
+
break;
|
|
76
|
+
seen.add(node);
|
|
77
|
+
const raw = node.raw;
|
|
78
|
+
if (raw !== undefined && raw.length > 0) {
|
|
79
|
+
const hash = certSpkiSha256(node, raw);
|
|
80
|
+
if (hash !== undefined && DISTRUSTED_SPKI_SHA256.has(hash))
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
const issuer = node.issuerCertificate;
|
|
84
|
+
if (issuer === undefined || issuer === node)
|
|
85
|
+
break;
|
|
86
|
+
node = issuer;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
export function makeCertChainGuard(prev = tls.checkServerIdentity) {
|
|
95
|
+
return (host, cert) => {
|
|
96
|
+
const prevResult = prev(host, cert);
|
|
97
|
+
if (prevResult)
|
|
98
|
+
return prevResult;
|
|
99
|
+
return chainHasCompromisedKey(cert) ? new CompromisedKeyError() : undefined;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export const guardCertChain = makeCertChainGuard();
|
|
103
|
+
const DISPATCHER_KEYS = [Symbol.for("undici.globalDispatcher.1"), Symbol.for("undici.globalDispatcher.2")];
|
|
104
|
+
let guardedAgent;
|
|
105
|
+
function isNode() {
|
|
106
|
+
try {
|
|
107
|
+
const v = typeof process === "object" ? process.versions : undefined;
|
|
108
|
+
return Boolean(v?.["node"]) && !v?.["bun"] && !v?.["deno"];
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function undiciAgentCtor() {
|
|
115
|
+
for (const key of DISPATCHER_KEYS) {
|
|
116
|
+
const d = globalThis[key];
|
|
117
|
+
const ctor = d?.constructor;
|
|
118
|
+
if (typeof ctor === "function" && ctor.name === "Agent")
|
|
119
|
+
return ctor;
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
export function guardedFetchDispatcher() {
|
|
124
|
+
if (guardedAgent !== undefined)
|
|
125
|
+
return guardedAgent === null ? undefined : guardedAgent;
|
|
126
|
+
guardedAgent = null;
|
|
127
|
+
try {
|
|
128
|
+
if (!isNode())
|
|
129
|
+
return undefined;
|
|
130
|
+
const Agent = undiciAgentCtor();
|
|
131
|
+
if (Agent === undefined)
|
|
132
|
+
return undefined;
|
|
133
|
+
const agent = new Agent({ connect: { checkServerIdentity: guardCertChain } });
|
|
134
|
+
if (agent !== null && typeof agent.dispatch === "function") {
|
|
135
|
+
guardedAgent = agent;
|
|
136
|
+
return agent;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
export const __caGuardInternals = {
|
|
144
|
+
distrustedCount() {
|
|
145
|
+
return DISTRUSTED_SPKI_SHA256.size;
|
|
146
|
+
},
|
|
147
|
+
has(hashHex) {
|
|
148
|
+
return DISTRUSTED_SPKI_SHA256.has(hashHex);
|
|
149
|
+
},
|
|
150
|
+
addForTest(hashHex) {
|
|
151
|
+
DISTRUSTED_SPKI_SHA256.add(hashHex);
|
|
152
|
+
TEST_HASHES.add(hashHex);
|
|
153
|
+
},
|
|
154
|
+
testHashes() {
|
|
155
|
+
return [...TEST_HASHES];
|
|
156
|
+
},
|
|
157
|
+
parses() {
|
|
158
|
+
return parseCount;
|
|
159
|
+
},
|
|
160
|
+
cacheSize() {
|
|
161
|
+
return SPKI_BY_FINGERPRINT.size;
|
|
162
|
+
},
|
|
163
|
+
resetAgent() {
|
|
164
|
+
guardedAgent = undefined;
|
|
165
|
+
},
|
|
166
|
+
};
|