sello 0.1.0 → 0.1.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 +2 -0
- package/dist/cbor.js +337 -0
- package/dist/cli/bench.js +389 -0
- package/dist/cli/demo.js +113 -0
- package/dist/cli/sello.js +515 -0
- package/dist/cose/protected-header.js +210 -0
- package/dist/cose/sign1.js +124 -0
- package/dist/crypto/ed25519.js +117 -0
- package/dist/crypto/identifiers.js +64 -0
- package/dist/hpke/base.js +349 -0
- package/dist/hpke/receipt.js +79 -0
- package/dist/index.js +15 -0
- package/dist/log/canonical-url.js +168 -0
- package/dist/log/mock-log.js +147 -0
- package/dist/log/rekor.js +120 -0
- package/dist/log/types.js +0 -0
- package/dist/mcp/middleware.js +162 -0
- package/dist/owner/verify.js +271 -0
- package/dist/receipt/body.js +210 -0
- package/dist/registry/json-registry.js +233 -0
- package/dist/sdk/index.js +22 -0
- package/dist/sdk/keys.js +191 -0
- package/dist/sdk/logs.js +196 -0
- package/dist/sdk/publisher.js +106 -0
- package/dist/sdk/service.js +561 -0
- package/dist/service/create-receipt.js +174 -0
- package/dist/token/jws-profile.js +174 -0
- package/docs/decisions.md +2 -2
- package/docs/release-checklist.md +4 -3
- package/docs/sdk-quickstart.md +2 -0
- package/package.json +10 -6
- package/src/cli/sello.ts +5 -3
package/dist/sdk/logs.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { toHex } from "../crypto/identifiers.js";
|
|
2
|
+
import {
|
|
3
|
+
|
|
4
|
+
assertCanonicalLogUrl,
|
|
5
|
+
} from "../log/canonical-url.js";
|
|
6
|
+
import { MockTransparencyLog } from "../log/mock-log.js";
|
|
7
|
+
import { base64urlEncode, decodeBase64url } from "./keys.js";
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
export function memory(url ) {
|
|
35
|
+
return new MockTransparencyLog(toCanonicalLogUrl(url));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function http(url , options = {}) {
|
|
39
|
+
return new HttpSelloLog(url, options);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function queryHttpLogByTokenRef(
|
|
43
|
+
input
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
,
|
|
49
|
+
) {
|
|
50
|
+
const fetcher = input.fetch ?? fetch;
|
|
51
|
+
const url = buildUrl(input.endpoint, `/entries?sello_token_ref=${toHex(input.tokenRef)}`);
|
|
52
|
+
const response = await fetcher(url, {
|
|
53
|
+
method: "GET",
|
|
54
|
+
headers: input.headers,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
throw new TypeError(`Sello log query failed with HTTP ${response.status}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const decoded = await response.json();
|
|
62
|
+
if (!isRecord(decoded) || !Array.isArray(decoded.entries)) {
|
|
63
|
+
throw new TypeError("Sello log query response must contain entries");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const completeness =
|
|
67
|
+
decoded.completeness === "complete" ? "complete" : "discovery-only";
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
completeness,
|
|
71
|
+
entries: decoded.entries.map(deserializeEntry),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class HttpSelloLog {
|
|
76
|
+
logUrl ;
|
|
77
|
+
endpoint ;
|
|
78
|
+
#headers ;
|
|
79
|
+
#fetch ;
|
|
80
|
+
|
|
81
|
+
constructor(url , options = {}) {
|
|
82
|
+
this.logUrl = toCanonicalLogUrl(options.logUrl ?? url);
|
|
83
|
+
this.endpoint = normalizeEndpoint(options.endpoint ?? url);
|
|
84
|
+
this.#headers = options.headers;
|
|
85
|
+
this.#fetch = options.fetch ?? fetch;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async append(
|
|
89
|
+
envelope ,
|
|
90
|
+
integratedTime ,
|
|
91
|
+
) {
|
|
92
|
+
const response = await this.#fetch(buildUrl(this.endpoint, "/entries"), {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: {
|
|
95
|
+
"content-type": "application/json",
|
|
96
|
+
...this.#headers,
|
|
97
|
+
},
|
|
98
|
+
body: JSON.stringify({
|
|
99
|
+
logUrl: this.logUrl,
|
|
100
|
+
envelope: base64urlEncode(envelope),
|
|
101
|
+
...(integratedTime === undefined ? {} : { integratedTime }),
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new TypeError(`Sello log append failed with HTTP ${response.status}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return deserializeEntry(await response.json());
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function serializeEntry(
|
|
114
|
+
entry ,
|
|
115
|
+
) {
|
|
116
|
+
return {
|
|
117
|
+
logUrl: entry.logUrl,
|
|
118
|
+
index: entry.index,
|
|
119
|
+
integratedTime: entry.integratedTime,
|
|
120
|
+
envelope: base64urlEncode(entry.envelope),
|
|
121
|
+
proof: cloneJson(entry.proof),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function deserializeEntry(input ) {
|
|
126
|
+
if (!isRecord(input)) {
|
|
127
|
+
throw new TypeError("Sello log entry must be an object");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const { logUrl, index, integratedTime, envelope, proof } = input;
|
|
131
|
+
assertCanonicalLogUrl(logUrl, "entry.logUrl");
|
|
132
|
+
|
|
133
|
+
if (!Number.isSafeInteger(index) || index < 0) {
|
|
134
|
+
throw new TypeError("entry.index must be a non-negative safe integer");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (
|
|
138
|
+
typeof integratedTime !== "string" ||
|
|
139
|
+
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(integratedTime)
|
|
140
|
+
) {
|
|
141
|
+
throw new TypeError("entry.integratedTime must be an RFC 3339 UTC timestamp");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (typeof envelope !== "string") {
|
|
145
|
+
throw new TypeError("entry.envelope must be base64url");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
logUrl,
|
|
150
|
+
index,
|
|
151
|
+
integratedTime,
|
|
152
|
+
envelope: decodeBase64url(envelope, "entry.envelope"),
|
|
153
|
+
proof: cloneJson(proof),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function toCanonicalLogUrl(url ) {
|
|
158
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
159
|
+
throw new TypeError("log URL must be a non-empty string");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const parsed = new URL(url);
|
|
163
|
+
const path = parsed.pathname === "/" ? "/api" : parsed.pathname;
|
|
164
|
+
const protocol =
|
|
165
|
+
parsed.protocol === "http:" && isLocalHost(parsed.hostname) ? "https:" : parsed.protocol;
|
|
166
|
+
const canonical = `${protocol}//${parsed.host}${path}`;
|
|
167
|
+
|
|
168
|
+
assertCanonicalLogUrl(canonical, "logUrl");
|
|
169
|
+
return canonical;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function normalizeEndpoint(url ) {
|
|
173
|
+
const parsed = new URL(url);
|
|
174
|
+
const path = parsed.pathname === "/" ? "/api" : parsed.pathname.replace(/\/$/, "");
|
|
175
|
+
return `${parsed.protocol}//${parsed.host}${path}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildUrl(endpoint , path ) {
|
|
179
|
+
return `${endpoint.replace(/\/$/, "")}${path}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isLocalHost(hostname ) {
|
|
183
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isRecord(value ) {
|
|
187
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function cloneJson(value ) {
|
|
191
|
+
if (value === null || typeof value !== "object") {
|
|
192
|
+
return value;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return globalThis.structuredClone(value);
|
|
196
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export class BackgroundReceiptPublisher {
|
|
2
|
+
#log ;
|
|
3
|
+
#mode ;
|
|
4
|
+
#maxPending ;
|
|
5
|
+
#concurrency ;
|
|
6
|
+
#onSubmitError ;
|
|
7
|
+
#onDrop ;
|
|
8
|
+
#queue = [];
|
|
9
|
+
#inFlight = new Set ();
|
|
10
|
+
|
|
11
|
+
constructor(input ) {
|
|
12
|
+
this.#log = input.log;
|
|
13
|
+
const options = normalizeSubmitOptions(input.submit);
|
|
14
|
+
this.#mode = options.mode;
|
|
15
|
+
this.#maxPending = options.maxPending;
|
|
16
|
+
this.#concurrency = options.concurrency;
|
|
17
|
+
this.#onSubmitError = input.onSubmitError;
|
|
18
|
+
this.#onDrop = input.onDrop;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get mode() {
|
|
22
|
+
return this.#mode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async publish(job ) {
|
|
26
|
+
if (this.#mode === "await") {
|
|
27
|
+
return await this.#log.append(job.envelope, job.integratedTime);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (this.#queue.length + this.#inFlight.size >= this.#maxPending) {
|
|
31
|
+
this.#onDrop?.({
|
|
32
|
+
envelope: new Uint8Array(job.envelope),
|
|
33
|
+
integratedTime: job.integratedTime,
|
|
34
|
+
reason: "queue_full",
|
|
35
|
+
});
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const promise = new Promise ((resolve, reject) => {
|
|
40
|
+
this.#queue.push({
|
|
41
|
+
envelope: new Uint8Array(job.envelope),
|
|
42
|
+
integratedTime: job.integratedTime,
|
|
43
|
+
resolve,
|
|
44
|
+
reject,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
this.#drain();
|
|
48
|
+
return promise;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
publishBackground(job ) {
|
|
52
|
+
void this.publish(job).catch((error) => {
|
|
53
|
+
this.#onSubmitError?.(error);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async flush() {
|
|
58
|
+
while (this.#queue.length > 0 || this.#inFlight.size > 0) {
|
|
59
|
+
await Promise.allSettled([...this.#inFlight]);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#drain() {
|
|
64
|
+
while (this.#inFlight.size < this.#concurrency && this.#queue.length > 0) {
|
|
65
|
+
const job = this.#queue.shift() ;
|
|
66
|
+
const task = this.#submit(job).finally(() => {
|
|
67
|
+
this.#inFlight.delete(task);
|
|
68
|
+
this.#drain();
|
|
69
|
+
});
|
|
70
|
+
this.#inFlight.add(task);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async #submit(job ) {
|
|
75
|
+
try {
|
|
76
|
+
const entry = await this.#log.append(job.envelope, job.integratedTime);
|
|
77
|
+
job.resolve(entry);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
this.#onSubmitError?.(error);
|
|
80
|
+
job.reject(error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizeSubmitOptions(
|
|
86
|
+
submit ,
|
|
87
|
+
) {
|
|
88
|
+
const options =
|
|
89
|
+
typeof submit === "string" || submit === undefined ? { mode: submit } : submit;
|
|
90
|
+
const mode = options.mode ?? "background";
|
|
91
|
+
if (mode !== "background" && mode !== "await") {
|
|
92
|
+
throw new TypeError("submit.mode must be background or await");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const maxPending = options.maxPending ?? 1000;
|
|
96
|
+
if (!Number.isSafeInteger(maxPending) || maxPending < 0) {
|
|
97
|
+
throw new TypeError("submit.maxPending must be a non-negative safe integer");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const concurrency = options.concurrency ?? 4;
|
|
101
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1) {
|
|
102
|
+
throw new TypeError("submit.concurrency must be a positive safe integer");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { mode, maxPending, concurrency };
|
|
106
|
+
}
|