@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/dist/caller.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `setCaller` — first-party attribution of SDK traffic.
|
|
3
|
+
*
|
|
4
|
+
* A caller is a short label naming the framework, harness or adapter driving this SDK: `cli`,
|
|
5
|
+
* `harbor`, `inspect`, or whatever you set yourself. It rides on one request header and on the
|
|
6
|
+
* `user-agent`, so we can see which integrations are actually used and build the next one where
|
|
7
|
+
* it is wanted.
|
|
8
|
+
*
|
|
9
|
+
* ## Nothing is ever auto-detected
|
|
10
|
+
*
|
|
11
|
+
* This module does not look at your process, your packages, your paths or your code, and it
|
|
12
|
+
* never will. There is no scan of `node_modules`, no walk of the module cache, no read of
|
|
13
|
+
* `argv`, no inspection of the process tree — nothing that could turn "which framework is this?"
|
|
14
|
+
* into a report about a customer's machine. Scanning a customer's process and phoning home about
|
|
15
|
+
* what is installed there is a reputational risk we are not taking, and a label we guessed would
|
|
16
|
+
* be worth less than one we were given. We only ever report a label we ship ourselves or one the
|
|
17
|
+
* customer sets on purpose.
|
|
18
|
+
*
|
|
19
|
+
* What is sent: a name, and optionally a version. That is the whole payload.
|
|
20
|
+
*
|
|
21
|
+
* ## Precedence (highest first)
|
|
22
|
+
*
|
|
23
|
+
* 1. `caller` in the client options
|
|
24
|
+
* 2. `setCaller(name, version)`
|
|
25
|
+
* 3. `PROMIGENCE_CALLER` (`name` or `name/version`)
|
|
26
|
+
* 4. the default an adapter package set with `setCaller(name, version, { default: true })`
|
|
27
|
+
* 5. none — and none is the default, so an SDK nobody configured sends no label at all
|
|
28
|
+
*
|
|
29
|
+
* ## Validation
|
|
30
|
+
*
|
|
31
|
+
* `name` must match `^[a-z0-9][a-z0-9._-]{0,31}$` after trimming and lower-casing; `version` must
|
|
32
|
+
* match `^[A-Za-z0-9.+_~-]{0,31}$`. Anything else is dropped silently — an invalid name sends no
|
|
33
|
+
* label, an invalid version sends the name alone. Telemetry never raises, never logs to stdout
|
|
34
|
+
* and never stops a request: the work is what matters, the label is not.
|
|
35
|
+
*/
|
|
36
|
+
/** The request header the label rides on. */
|
|
37
|
+
export declare const CALLER_HEADER = "promigence-caller";
|
|
38
|
+
/** `PROMIGENCE_CALLER=harbor` or `PROMIGENCE_CALLER=harbor/0.1.0`. */
|
|
39
|
+
export declare const CALLER_ENV = "PROMIGENCE_CALLER";
|
|
40
|
+
export declare const CALLER_NAME_RE: RegExp;
|
|
41
|
+
export declare const CALLER_VERSION_RE: RegExp;
|
|
42
|
+
export declare const RESERVED_CALLERS: readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* The token for `name` (+ `version`), or `undefined` when the name is not a valid label.
|
|
45
|
+
* An invalid version is dropped on its own, leaving the name.
|
|
46
|
+
*/
|
|
47
|
+
export declare function callerToken(name: string, version?: string): string | undefined;
|
|
48
|
+
/** `harbor` or `harbor/0.1.0` as one string (the env var, the `caller` option). */
|
|
49
|
+
export declare function parseCallerToken(raw: string | undefined): string | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Name the framework or harness driving this SDK.
|
|
52
|
+
*
|
|
53
|
+
* ```ts
|
|
54
|
+
* import { setCaller } from "promigence";
|
|
55
|
+
* setCaller("my-harness", "2.1.0");
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* `{ default: true }` is for a package that ships an integration: it sets the label only for a
|
|
59
|
+
* customer who has not set one of their own, and `PROMIGENCE_CALLER` still wins over it.
|
|
60
|
+
*
|
|
61
|
+
* An unusable name is ignored. This never throws.
|
|
62
|
+
*/
|
|
63
|
+
export declare function setCaller(name: string, version?: string, options?: {
|
|
64
|
+
default?: boolean;
|
|
65
|
+
}): void;
|
|
66
|
+
/** The label that would be sent, ignoring any per-client `caller` option. */
|
|
67
|
+
export declare function getCaller(): string | undefined;
|
|
68
|
+
/** Everything unset again (tests). */
|
|
69
|
+
export declare function clearCaller(): void;
|
|
70
|
+
/** The label one client sends: its own option first, then everything `getCaller` considers. */
|
|
71
|
+
export declare function resolveCaller(explicit?: string): string | undefined;
|
|
72
|
+
/** `promigence/0.1.0 node/22.0.0` + ` (harbor/0.1.0)`, when there is a label to add. */
|
|
73
|
+
export declare function userAgentWithCaller(userAgent: string, caller: string | undefined): string;
|
package/dist/caller.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export const CALLER_HEADER = "promigence-caller";
|
|
2
|
+
export const CALLER_ENV = "PROMIGENCE_CALLER";
|
|
3
|
+
export const CALLER_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,31}$/;
|
|
4
|
+
export const CALLER_VERSION_RE = /^[A-Za-z0-9.+_~-]{0,31}$/;
|
|
5
|
+
export const RESERVED_CALLERS = [
|
|
6
|
+
"cli", "harbor", "inspect", "e2b-compat", "computesdk", "mcp",
|
|
7
|
+
];
|
|
8
|
+
let globalCaller;
|
|
9
|
+
let defaultCaller;
|
|
10
|
+
export function callerToken(name, version) {
|
|
11
|
+
const n = typeof name === "string" ? name.trim().toLowerCase() : "";
|
|
12
|
+
if (!CALLER_NAME_RE.test(n))
|
|
13
|
+
return undefined;
|
|
14
|
+
const v = typeof version === "string" ? version.trim() : "";
|
|
15
|
+
if (!v || !CALLER_VERSION_RE.test(v))
|
|
16
|
+
return n;
|
|
17
|
+
return `${n}/${v}`;
|
|
18
|
+
}
|
|
19
|
+
export function parseCallerToken(raw) {
|
|
20
|
+
if (typeof raw !== "string")
|
|
21
|
+
return undefined;
|
|
22
|
+
const t = raw.trim();
|
|
23
|
+
if (!t)
|
|
24
|
+
return undefined;
|
|
25
|
+
const slash = t.indexOf("/");
|
|
26
|
+
return slash < 0 ? callerToken(t) : callerToken(t.slice(0, slash), t.slice(slash + 1));
|
|
27
|
+
}
|
|
28
|
+
export function setCaller(name, version, options) {
|
|
29
|
+
const token = callerToken(name, version);
|
|
30
|
+
if (options?.default)
|
|
31
|
+
defaultCaller = token;
|
|
32
|
+
else
|
|
33
|
+
globalCaller = token;
|
|
34
|
+
}
|
|
35
|
+
export function getCaller() {
|
|
36
|
+
return globalCaller ?? parseCallerToken(process.env[CALLER_ENV]) ?? defaultCaller;
|
|
37
|
+
}
|
|
38
|
+
export function clearCaller() {
|
|
39
|
+
globalCaller = undefined;
|
|
40
|
+
defaultCaller = undefined;
|
|
41
|
+
}
|
|
42
|
+
export function resolveCaller(explicit) {
|
|
43
|
+
return parseCallerToken(explicit) ?? getCaller();
|
|
44
|
+
}
|
|
45
|
+
export function userAgentWithCaller(userAgent, caller) {
|
|
46
|
+
return caller ? `${userAgent} (${caller})` : userAgent;
|
|
47
|
+
}
|
|
@@ -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;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.blake3Ref = void 0;
|
|
4
|
+
exports.blake3Hex = blake3Hex;
|
|
5
|
+
const IV = new Uint32Array([
|
|
6
|
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
|
7
|
+
]);
|
|
8
|
+
const PERM = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8];
|
|
9
|
+
const CHUNK_START = 1, CHUNK_END = 2, PARENT = 4, ROOT = 8;
|
|
10
|
+
const BLOCK_LEN = 64, CHUNK_LEN = 1024;
|
|
11
|
+
const rotr = (x, n) => ((x >>> n) | (x << (32 - n))) >>> 0;
|
|
12
|
+
function g(s, a, b, c, d, mx, my) {
|
|
13
|
+
s[a] = (s[a] + s[b] + mx) >>> 0;
|
|
14
|
+
s[d] = rotr(s[d] ^ s[a], 16);
|
|
15
|
+
s[c] = (s[c] + s[d]) >>> 0;
|
|
16
|
+
s[b] = rotr(s[b] ^ s[c], 12);
|
|
17
|
+
s[a] = (s[a] + s[b] + my) >>> 0;
|
|
18
|
+
s[d] = rotr(s[d] ^ s[a], 8);
|
|
19
|
+
s[c] = (s[c] + s[d]) >>> 0;
|
|
20
|
+
s[b] = rotr(s[b] ^ s[c], 7);
|
|
21
|
+
}
|
|
22
|
+
function compress(cv, m, counter, blockLen, flags) {
|
|
23
|
+
const s = new Uint32Array(16);
|
|
24
|
+
for (let i = 0; i < 8; i++)
|
|
25
|
+
s[i] = cv[i];
|
|
26
|
+
for (let i = 0; i < 4; i++)
|
|
27
|
+
s[8 + i] = IV[i];
|
|
28
|
+
s[12] = counter >>> 0;
|
|
29
|
+
s[13] = Math.floor(counter / 0x100000000) >>> 0;
|
|
30
|
+
s[14] = blockLen >>> 0;
|
|
31
|
+
s[15] = flags >>> 0;
|
|
32
|
+
let x = m;
|
|
33
|
+
for (let r = 0; r < 7; r++) {
|
|
34
|
+
g(s, 0, 4, 8, 12, x[0], x[1]);
|
|
35
|
+
g(s, 1, 5, 9, 13, x[2], x[3]);
|
|
36
|
+
g(s, 2, 6, 10, 14, x[4], x[5]);
|
|
37
|
+
g(s, 3, 7, 11, 15, x[6], x[7]);
|
|
38
|
+
g(s, 0, 5, 10, 15, x[8], x[9]);
|
|
39
|
+
g(s, 1, 6, 11, 12, x[10], x[11]);
|
|
40
|
+
g(s, 2, 7, 8, 13, x[12], x[13]);
|
|
41
|
+
g(s, 3, 4, 9, 14, x[14], x[15]);
|
|
42
|
+
if (r < 6) {
|
|
43
|
+
const y = new Uint32Array(16);
|
|
44
|
+
for (let i = 0; i < 16; i++)
|
|
45
|
+
y[i] = x[PERM[i]];
|
|
46
|
+
x = y;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (let i = 0; i < 8; i++) {
|
|
50
|
+
s[i] = (s[i] ^ s[i + 8]) >>> 0;
|
|
51
|
+
s[i + 8] = (s[i + 8] ^ cv[i]) >>> 0;
|
|
52
|
+
}
|
|
53
|
+
return s;
|
|
54
|
+
}
|
|
55
|
+
function words(b, off, len) {
|
|
56
|
+
const m = new Uint32Array(16);
|
|
57
|
+
for (let i = 0; i < len; i++)
|
|
58
|
+
m[i >> 2] |= b[off + i] << ((i & 3) * 8);
|
|
59
|
+
for (let i = 0; i < 16; i++)
|
|
60
|
+
m[i] = m[i] >>> 0;
|
|
61
|
+
return m;
|
|
62
|
+
}
|
|
63
|
+
const chainingValue = (o) => compress(o.cv, o.m, o.counter, o.blockLen, o.flags).slice(0, 8);
|
|
64
|
+
function chunkOutput(b, off, len, counter) {
|
|
65
|
+
let cv = IV;
|
|
66
|
+
const blocks = Math.max(1, Math.ceil(len / BLOCK_LEN));
|
|
67
|
+
for (let i = 0; i < blocks - 1; i++) {
|
|
68
|
+
const flags = i === 0 ? CHUNK_START : 0;
|
|
69
|
+
cv = compress(cv, words(b, off + i * BLOCK_LEN, BLOCK_LEN), counter, BLOCK_LEN, flags).slice(0, 8);
|
|
70
|
+
}
|
|
71
|
+
const last = (blocks - 1) * BLOCK_LEN;
|
|
72
|
+
return {
|
|
73
|
+
cv,
|
|
74
|
+
m: words(b, off + last, len - last),
|
|
75
|
+
counter,
|
|
76
|
+
blockLen: len - last,
|
|
77
|
+
flags: (blocks === 1 ? CHUNK_START : 0) | CHUNK_END,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function parentOutput(l, r) {
|
|
81
|
+
const m = new Uint32Array(16);
|
|
82
|
+
m.set(l, 0);
|
|
83
|
+
m.set(r, 8);
|
|
84
|
+
return { cv: IV, m, counter: 0, blockLen: BLOCK_LEN, flags: PARENT };
|
|
85
|
+
}
|
|
86
|
+
function leftLen(n) {
|
|
87
|
+
let l = CHUNK_LEN;
|
|
88
|
+
while (l * 2 < n)
|
|
89
|
+
l *= 2;
|
|
90
|
+
return l;
|
|
91
|
+
}
|
|
92
|
+
function subtree(b, off, len, counter) {
|
|
93
|
+
if (len <= CHUNK_LEN)
|
|
94
|
+
return chainingValue(chunkOutput(b, off, len, counter));
|
|
95
|
+
const ll = leftLen(len);
|
|
96
|
+
return chainingValue(parentOutput(subtree(b, off, ll, counter), subtree(b, off + ll, len - ll, counter + ll / CHUNK_LEN)));
|
|
97
|
+
}
|
|
98
|
+
function blake3Hex(input) {
|
|
99
|
+
const n = input.length;
|
|
100
|
+
const root = n <= CHUNK_LEN
|
|
101
|
+
? chunkOutput(input, 0, n, 0)
|
|
102
|
+
: (() => {
|
|
103
|
+
const ll = leftLen(n);
|
|
104
|
+
return parentOutput(subtree(input, 0, ll, 0), subtree(input, ll, n - ll, ll / CHUNK_LEN));
|
|
105
|
+
})();
|
|
106
|
+
const s = compress(root.cv, root.m, 0, root.blockLen, root.flags | ROOT);
|
|
107
|
+
let out = "";
|
|
108
|
+
for (let i = 0; i < 8; i++) {
|
|
109
|
+
const w = s[i];
|
|
110
|
+
for (let k = 0; k < 4; k++)
|
|
111
|
+
out += (((w >>> (k * 8)) & 0xff) | 0x100).toString(16).slice(1);
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
const blake3Ref = (text) => `blake3:${blake3Hex(new TextEncoder().encode(text))}`;
|
|
116
|
+
exports.blake3Ref = blake3Ref;
|
|
@@ -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;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BundleVerifyError = exports.platformOf = exports.renderBundle = exports.canonicalBundle = exports.digestOf = exports.BUNDLE_EXT = exports.MANIFEST_SCHEMA = exports.BUNDLE_SCHEMA = void 0;
|
|
4
|
+
exports.canonicalize = canonicalize;
|
|
5
|
+
exports.pretty = pretty;
|
|
6
|
+
exports.strip = strip;
|
|
7
|
+
exports.buildBundle = buildBundle;
|
|
8
|
+
exports.bundleForm = bundleForm;
|
|
9
|
+
exports.verifyBundle = verifyBundle;
|
|
10
|
+
exports.parseBundle = parseBundle;
|
|
11
|
+
const blake3_js_1 = require("./blake3.js");
|
|
12
|
+
exports.BUNDLE_SCHEMA = "promigence.bundle/v1";
|
|
13
|
+
exports.MANIFEST_SCHEMA = "promigence.snapshot/v1";
|
|
14
|
+
exports.BUNDLE_EXT = ".promigence";
|
|
15
|
+
function canonicalize(v) {
|
|
16
|
+
if (v === null)
|
|
17
|
+
return "null";
|
|
18
|
+
switch (typeof v) {
|
|
19
|
+
case "boolean": return v ? "true" : "false";
|
|
20
|
+
case "number":
|
|
21
|
+
if (!Number.isFinite(v))
|
|
22
|
+
throw new TypeError(`non-finite number cannot be canonicalised: ${v}`);
|
|
23
|
+
return Object.is(v, -0) ? "0" : String(v);
|
|
24
|
+
case "string": return JSON.stringify(v);
|
|
25
|
+
case "object": break;
|
|
26
|
+
default: throw new TypeError(`cannot canonicalise ${typeof v}`);
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(v))
|
|
29
|
+
return `[${v.map(canonicalize).join(",")}]`;
|
|
30
|
+
const o = v;
|
|
31
|
+
const keys = Object.keys(o).filter((k) => o[k] !== undefined).sort();
|
|
32
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(o[k])}`).join(",")}}`;
|
|
33
|
+
}
|
|
34
|
+
const digestOf = (v) => (0, blake3_js_1.blake3Ref)(canonicalize(v));
|
|
35
|
+
exports.digestOf = digestOf;
|
|
36
|
+
function pretty(canonical) {
|
|
37
|
+
let out = "";
|
|
38
|
+
let depth = 0, inStr = false, esc = false;
|
|
39
|
+
for (let i = 0; i < canonical.length; i++) {
|
|
40
|
+
const c = canonical[i];
|
|
41
|
+
if (inStr) {
|
|
42
|
+
out += c;
|
|
43
|
+
if (esc)
|
|
44
|
+
esc = false;
|
|
45
|
+
else if (c === "\\")
|
|
46
|
+
esc = true;
|
|
47
|
+
else if (c === '"')
|
|
48
|
+
inStr = false;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (c === '"') {
|
|
52
|
+
inStr = true;
|
|
53
|
+
out += c;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (c === "{" || c === "[") {
|
|
57
|
+
const close = c === "{" ? "}" : "]";
|
|
58
|
+
if (canonical[i + 1] === close) {
|
|
59
|
+
out += c + close;
|
|
60
|
+
i++;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
depth++;
|
|
64
|
+
out += c + "\n" + " ".repeat(depth);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (c === "}" || c === "]") {
|
|
68
|
+
depth = Math.max(0, depth - 1);
|
|
69
|
+
out += "\n" + " ".repeat(depth) + c;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (c === ",") {
|
|
73
|
+
out += ",\n" + " ".repeat(depth);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (c === ":") {
|
|
77
|
+
out += ": ";
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
out += c;
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
function strip(text) {
|
|
85
|
+
let out = "";
|
|
86
|
+
let inStr = false, esc = false;
|
|
87
|
+
for (const c of text) {
|
|
88
|
+
if (inStr) {
|
|
89
|
+
out += c;
|
|
90
|
+
if (esc)
|
|
91
|
+
esc = false;
|
|
92
|
+
else if (c === "\\")
|
|
93
|
+
esc = true;
|
|
94
|
+
else if (c === '"')
|
|
95
|
+
inStr = false;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (c === '"') {
|
|
99
|
+
inStr = true;
|
|
100
|
+
out += c;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (c === " " || c === "\t" || c === "\n" || c === "\r")
|
|
104
|
+
continue;
|
|
105
|
+
out += c;
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
const idOf = (hash) => `snap_${hash.slice(7, 7 + 24)}`;
|
|
110
|
+
function buildBundle(manifest, opts = {}) {
|
|
111
|
+
const sealedIn = manifest?.["sealed"];
|
|
112
|
+
if (!sealedIn || typeof sealedIn !== "object" || Array.isArray(sealedIn)) {
|
|
113
|
+
throw new BundleVerifyError("schema", "a bundle is built from a sealed manifest, and this one is not sealed");
|
|
114
|
+
}
|
|
115
|
+
const alias = opts.alias ?? manifest["alias"] ?? undefined;
|
|
116
|
+
const s = sealedIn;
|
|
117
|
+
const hash = String(manifest["manifest_hash"] ?? "");
|
|
118
|
+
return {
|
|
119
|
+
schema: exports.BUNDLE_SCHEMA,
|
|
120
|
+
snap_id: idOf(hash),
|
|
121
|
+
manifest_hash: hash,
|
|
122
|
+
...(alias ? { alias } : {}),
|
|
123
|
+
digests: {
|
|
124
|
+
provenance: (0, exports.digestOf)(manifest["provenance"]),
|
|
125
|
+
verification: (0, exports.digestOf)(manifest["verification"]),
|
|
126
|
+
},
|
|
127
|
+
layers: [],
|
|
128
|
+
sealed: { platform: String(s["platform"] ?? ""), machine: String(s["machine"] ?? ""), layers: String(s["layers"] ?? "") },
|
|
129
|
+
provenance: manifest["provenance"],
|
|
130
|
+
verification: manifest["verification"],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const canonicalBundle = (b) => canonicalize(b);
|
|
134
|
+
exports.canonicalBundle = canonicalBundle;
|
|
135
|
+
const renderBundle = (b) => pretty(canonicalize(b)) + "\n";
|
|
136
|
+
exports.renderBundle = renderBundle;
|
|
137
|
+
function bundleForm(b) {
|
|
138
|
+
if (b.sealed !== undefined)
|
|
139
|
+
return "sealed";
|
|
140
|
+
return b.layers_ref !== undefined ? "citation" : "full";
|
|
141
|
+
}
|
|
142
|
+
const platformOf = (b) => b.sealed?.platform;
|
|
143
|
+
exports.platformOf = platformOf;
|
|
144
|
+
class BundleVerifyError extends Error {
|
|
145
|
+
kind;
|
|
146
|
+
detail;
|
|
147
|
+
constructor(kind, message, detail = {}) {
|
|
148
|
+
super(message);
|
|
149
|
+
this.name = "BundleVerifyError";
|
|
150
|
+
this.kind = kind;
|
|
151
|
+
this.detail = detail;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
exports.BundleVerifyError = BundleVerifyError;
|
|
155
|
+
const HEX = (s, n) => new RegExp(`^[0-9a-f]{${n}}$`).test(s);
|
|
156
|
+
function checkHash(field, h) {
|
|
157
|
+
const s = String(h ?? "");
|
|
158
|
+
if (!s.startsWith("blake3:") || !HEX(s.slice(7), 64)) {
|
|
159
|
+
throw new BundleVerifyError("hash_format", `${field} ${JSON.stringify(s)} is not blake3:<64 lowercase hex>`, { field, found: s });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function section(name, recorded, v) {
|
|
163
|
+
const computed = (0, exports.digestOf)(v);
|
|
164
|
+
if (computed !== recorded) {
|
|
165
|
+
throw new BundleVerifyError("section_digest", `section \`${name}\` does not match its recorded digest: recorded ${recorded}, computed ${computed}`, { section: name, recorded, computed });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function checkId(b) {
|
|
169
|
+
const derived = idOf(b.manifest_hash);
|
|
170
|
+
if (derived !== b.snap_id) {
|
|
171
|
+
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 });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function verifyBundle(b) {
|
|
175
|
+
if (b?.schema !== exports.BUNDLE_SCHEMA) {
|
|
176
|
+
throw new BundleVerifyError("schema", `not a ${exports.BUNDLE_SCHEMA} document: schema is ${JSON.stringify(b?.schema ?? null)}`, { found: b?.schema });
|
|
177
|
+
}
|
|
178
|
+
if (!/^snap_[0-9a-f]{24}$/.test(b.snap_id ?? "")) {
|
|
179
|
+
throw new BundleVerifyError("snap_id_format", `snap_id ${JSON.stringify(b.snap_id ?? null)} is not snap_<24 lowercase hex>`, { found: b.snap_id });
|
|
180
|
+
}
|
|
181
|
+
checkHash("manifest_hash", b.manifest_hash);
|
|
182
|
+
const form = bundleForm(b);
|
|
183
|
+
if (form === "sealed") {
|
|
184
|
+
for (const k of ["platform", "machine", "layers"])
|
|
185
|
+
checkHash(`sealed.${k}`, b.sealed?.[k]);
|
|
186
|
+
for (const k of ["provenance", "verification"])
|
|
187
|
+
checkHash(`digests.${k}`, b.digests?.[k]);
|
|
188
|
+
section("provenance", b.digests.provenance, b.provenance);
|
|
189
|
+
section("verification", b.digests.verification, b.verification);
|
|
190
|
+
checkId(b);
|
|
191
|
+
return form;
|
|
192
|
+
}
|
|
193
|
+
const digests = b.digests && typeof b.digests === "object" ? { ...b.digests } : {};
|
|
194
|
+
const names = Object.keys(digests).sort();
|
|
195
|
+
for (const k of ["provenance", "verification"]) {
|
|
196
|
+
if (!names.includes(k))
|
|
197
|
+
throw new BundleVerifyError("schema", `digests.${k} is missing`, { form });
|
|
198
|
+
}
|
|
199
|
+
for (const k of names)
|
|
200
|
+
checkHash(`digests.${k}`, digests[k]);
|
|
201
|
+
let omitted;
|
|
202
|
+
if (form === "citation") {
|
|
203
|
+
const pinned = String(b.layers_ref?.digest ?? "");
|
|
204
|
+
checkHash("layers_ref.digest", pinned);
|
|
205
|
+
omitted = names.find((k) => digests[k] === pinned);
|
|
206
|
+
if (omitted === undefined) {
|
|
207
|
+
throw new BundleVerifyError("section_digest", `layers_ref.digest ${pinned} pins no section this bundle records`, { recorded: pinned });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for (const k of names) {
|
|
211
|
+
if (k === omitted)
|
|
212
|
+
continue;
|
|
213
|
+
if (b[k] === undefined)
|
|
214
|
+
throw new BundleVerifyError("schema", `section \`${k}\` is missing`, { section: k });
|
|
215
|
+
section(k, digests[k], b[k]);
|
|
216
|
+
}
|
|
217
|
+
if (form === "full") {
|
|
218
|
+
const content = { schema: exports.MANIFEST_SCHEMA };
|
|
219
|
+
for (const k of names)
|
|
220
|
+
content[k] = b[k];
|
|
221
|
+
const computed = (0, blake3_js_1.blake3Ref)(canonicalize(content));
|
|
222
|
+
if (computed !== b.manifest_hash) {
|
|
223
|
+
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 });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
checkId(b);
|
|
227
|
+
return form;
|
|
228
|
+
}
|
|
229
|
+
function parseBundle(text) {
|
|
230
|
+
let v;
|
|
231
|
+
try {
|
|
232
|
+
v = JSON.parse(text);
|
|
233
|
+
}
|
|
234
|
+
catch (e) {
|
|
235
|
+
throw new BundleVerifyError("canonical", `not JSON: ${e.message}`);
|
|
236
|
+
}
|
|
237
|
+
const b = v;
|
|
238
|
+
if (b?.schema !== exports.BUNDLE_SCHEMA) {
|
|
239
|
+
throw new BundleVerifyError("schema", `not a ${exports.BUNDLE_SCHEMA} document: schema is ${JSON.stringify(b?.schema ?? null)}`, { found: b?.schema });
|
|
240
|
+
}
|
|
241
|
+
return b;
|
|
242
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|