@agentmesh-p2p/sdk 0.0.0-stage → 0.1.0
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 +182 -2
- package/dist/auth.d.ts +66 -0
- package/dist/auth.js +164 -0
- package/dist/authorizer.d.ts +41 -0
- package/dist/authorizer.js +201 -0
- package/dist/base58.d.ts +2 -0
- package/dist/base58.js +57 -0
- package/dist/biscuit.d.ts +75 -0
- package/dist/biscuit.js +250 -0
- package/dist/bytes.d.ts +10 -0
- package/dist/bytes.js +51 -0
- package/dist/challenges.d.ts +16 -0
- package/dist/challenges.js +55 -0
- package/dist/conformance-join.d.ts +1 -0
- package/dist/conformance-join.js +257 -0
- package/dist/conformance.d.ts +1 -0
- package/dist/conformance.js +60 -0
- package/dist/controlplane.d.ts +123 -0
- package/dist/controlplane.js +368 -0
- package/dist/credential.d.ts +66 -0
- package/dist/credential.js +124 -0
- package/dist/discovery.d.ts +50 -0
- package/dist/discovery.js +150 -0
- package/dist/gen/agentmesh_pb.d.ts +2740 -0
- package/dist/gen/agentmesh_pb.js +591 -0
- package/dist/gen/datalog.d.ts +39 -0
- package/dist/gen/datalog.js +84 -0
- package/dist/host.d.ts +47 -0
- package/dist/host.js +109 -0
- package/dist/http1.d.ts +68 -0
- package/dist/http1.js +371 -0
- package/dist/identity.d.ts +31 -0
- package/dist/identity.js +125 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +30 -0
- package/dist/libp2p-http-node.d.ts +17 -0
- package/dist/libp2p-http-node.js +116 -0
- package/dist/libp2p-http.d.ts +165 -0
- package/dist/libp2p-http.js +581 -0
- package/dist/mcp.d.ts +67 -0
- package/dist/mcp.js +157 -0
- package/dist/mesh.d.ts +151 -0
- package/dist/mesh.js +370 -0
- package/dist/platform/ingress.browser.d.ts +1 -0
- package/dist/platform/ingress.browser.js +16 -0
- package/dist/platform/ingress.d.ts +1 -0
- package/dist/platform/ingress.js +15 -0
- package/dist/platform/state.browser.d.ts +5 -0
- package/dist/platform/state.browser.js +47 -0
- package/dist/platform/state.d.ts +5 -0
- package/dist/platform/state.js +44 -0
- package/dist/platform/transports.browser.d.ts +3 -0
- package/dist/platform/transports.browser.js +27 -0
- package/dist/platform/transports.d.ts +3 -0
- package/dist/platform/transports.js +27 -0
- package/dist/platform/types.d.ts +7 -0
- package/dist/platform/types.js +14 -0
- package/dist/platform/wasm.browser.d.ts +2 -0
- package/dist/platform/wasm.browser.js +16 -0
- package/dist/platform/wasm.d.ts +2 -0
- package/dist/platform/wasm.js +56 -0
- package/dist/session.d.ts +316 -0
- package/dist/session.js +1269 -0
- package/dist/sync.d.ts +41 -0
- package/dist/sync.js +111 -0
- package/dist/tar.d.ts +25 -0
- package/dist/tar.js +406 -0
- package/package.json +74 -4
package/dist/base58.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Copyright 2026 Google LLC
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
// base58btc, the alphabet libp2p peer IDs are written in.
|
|
15
|
+
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
16
|
+
const BASE = BigInt(ALPHABET.length);
|
|
17
|
+
const INDEX = new Map();
|
|
18
|
+
for (let i = 0; i < ALPHABET.length; i++) {
|
|
19
|
+
INDEX.set(ALPHABET.charAt(i), i);
|
|
20
|
+
}
|
|
21
|
+
export function encodeBase58(bytes) {
|
|
22
|
+
let zeros = 0;
|
|
23
|
+
while (zeros < bytes.length && bytes[zeros] === 0) {
|
|
24
|
+
zeros++;
|
|
25
|
+
}
|
|
26
|
+
let n = 0n;
|
|
27
|
+
for (const b of bytes) {
|
|
28
|
+
n = (n << 8n) | BigInt(b);
|
|
29
|
+
}
|
|
30
|
+
let out = "";
|
|
31
|
+
while (n > 0n) {
|
|
32
|
+
const digit = Number(n % BASE);
|
|
33
|
+
out = ALPHABET.charAt(digit) + out;
|
|
34
|
+
n /= BASE;
|
|
35
|
+
}
|
|
36
|
+
return "1".repeat(zeros) + out;
|
|
37
|
+
}
|
|
38
|
+
export function decodeBase58(text) {
|
|
39
|
+
let zeros = 0;
|
|
40
|
+
while (zeros < text.length && text.charAt(zeros) === "1") {
|
|
41
|
+
zeros++;
|
|
42
|
+
}
|
|
43
|
+
let n = 0n;
|
|
44
|
+
for (const ch of text) {
|
|
45
|
+
const digit = INDEX.get(ch);
|
|
46
|
+
if (digit === undefined) {
|
|
47
|
+
throw new Error(`invalid base58 character ${JSON.stringify(ch)}`);
|
|
48
|
+
}
|
|
49
|
+
n = n * BASE + BigInt(digit);
|
|
50
|
+
}
|
|
51
|
+
const body = [];
|
|
52
|
+
while (n > 0n) {
|
|
53
|
+
body.unshift(Number(n & 0xffn));
|
|
54
|
+
n >>= 8n;
|
|
55
|
+
}
|
|
56
|
+
return new Uint8Array([...new Array(zeros).fill(0), ...body]);
|
|
57
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { MessageInitShape } from "@bufbuild/protobuf";
|
|
2
|
+
import { TaskAuthorizationRuleSchema, type TaskAuthorizationRule } from "./gen/agentmesh_pb.ts";
|
|
3
|
+
import { type BiscuitWasm } from "./platform/wasm.ts";
|
|
4
|
+
/** The biscuit-wasm module, loaded once, the way the runtime loads it. */
|
|
5
|
+
export declare function loadBiscuit(): Promise<BiscuitWasm>;
|
|
6
|
+
/** Datalog evaluation budget, as in internal/identity.AuthorizerOptions. */
|
|
7
|
+
export declare const AUTHORIZER_LIMITS: {
|
|
8
|
+
max_facts: number;
|
|
9
|
+
max_iterations: number;
|
|
10
|
+
max_time_micro: number;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Runs an authorize or query call under AUTHORIZER_LIMITS. biscuit-wasm
|
|
14
|
+
* applies the limits it is passed to the checks and policies only; the
|
|
15
|
+
* fact-generation pass runs under the builder's own limits, 1 ms of wall
|
|
16
|
+
* clock, which it does not let a caller change. A paused event loop or a
|
|
17
|
+
* throttled CPU then answers a valid request with RunLimit Timeout. The
|
|
18
|
+
* facts derived before that deadline stay in the authorizer and the next
|
|
19
|
+
* call resumes from them, so the pass is driven to completion here until
|
|
20
|
+
* the budget above is spent.
|
|
21
|
+
*/
|
|
22
|
+
export declare function withinLimits<T>(run: () => T): T;
|
|
23
|
+
export declare const ROLE_ROUTER = "mesh:role:router";
|
|
24
|
+
export declare class BiscuitVerificationError extends Error {
|
|
25
|
+
constructor(message: string, options?: ErrorOptions);
|
|
26
|
+
}
|
|
27
|
+
/** What a verified peer biscuit says about its holder. */
|
|
28
|
+
export interface VerifiedBiscuit {
|
|
29
|
+
/** The peer the token is bound to (its node() or actor_node()/client_peer_id() fact). */
|
|
30
|
+
peerId: string;
|
|
31
|
+
/** The origin node channel when the token is a delegated session biscuit (actor_node()). */
|
|
32
|
+
actorNode?: string;
|
|
33
|
+
/** When the token lapses; the minimum of authority expiration() and any tar_block expire_time. */
|
|
34
|
+
expiration: Date;
|
|
35
|
+
/** The trusted key that verified the signature. */
|
|
36
|
+
verifyingKey: Uint8Array;
|
|
37
|
+
roles: string[];
|
|
38
|
+
labels: Record<string, string>;
|
|
39
|
+
/** Verified TaskAuthorizationRule chain from blocks 1..N (empty for unattenuated tokens). */
|
|
40
|
+
taskRules: TaskAuthorizationRule[];
|
|
41
|
+
/** The token as received, for what is decided over its facts after verification (requirePublish). */
|
|
42
|
+
token: Uint8Array;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Verifies a biscuit received from expectedPeerId over an authenticated
|
|
46
|
+
* connection. Every trusted key is tried, so a token minted under a
|
|
47
|
+
* retiring key still verifies during rotation.
|
|
48
|
+
*
|
|
49
|
+
* By default (peer handshakes on /mesh/auth/1.0.0), the authority block must
|
|
50
|
+
* carry node(expectedPeerId). When allowDelegated is true (request tokens in
|
|
51
|
+
* authorizeCaller), the authority block may alternatively carry both
|
|
52
|
+
* actor_node(expectedPeerId) and client_peer_id(expectedPeerId) without node().
|
|
53
|
+
*/
|
|
54
|
+
export declare function verifyPeerBiscuit(biscuitBytes: Uint8Array, expectedPeerId: string, trustedKeys: Uint8Array[], now?: Date, options?: {
|
|
55
|
+
allowDelegated?: boolean;
|
|
56
|
+
}): Promise<VerifiedBiscuit>;
|
|
57
|
+
/**
|
|
58
|
+
* Appends a non-authority block carrying a single tar_block("<base64url-proto>")
|
|
59
|
+
* fact to an existing Biscuit token in memory without contacting the control plane.
|
|
60
|
+
*/
|
|
61
|
+
export declare function attenuateBiscuit(biscuitBytes: Uint8Array, ruleInput: MessageInitShape<typeof TaskAuthorizationRuleSchema>, trustedKeys: Uint8Array[]): Promise<Uint8Array>;
|
|
62
|
+
/**
|
|
63
|
+
* Seals a Biscuit token so no further blocks can be appended by downstream holders.
|
|
64
|
+
*/
|
|
65
|
+
export declare function sealBiscuit(biscuitBytes: Uint8Array, trustedKeys: Uint8Array[]): Promise<Uint8Array>;
|
|
66
|
+
/** Requires role(<role>) on an already verified token, as identity.RequireRole. */
|
|
67
|
+
export declare function requireRole(verified: VerifiedBiscuit, role: string): void;
|
|
68
|
+
/**
|
|
69
|
+
* Requires that an already verified provider credential grants serving
|
|
70
|
+
* targetService ("<type>://<name>"), as identity.RequirePublish: the
|
|
71
|
+
* directory names whoever announced the service, only the credential says
|
|
72
|
+
* who may serve it. The provider's own catalog names no service and needs
|
|
73
|
+
* no grant; so does an empty target.
|
|
74
|
+
*/
|
|
75
|
+
export declare function requirePublish(verified: VerifiedBiscuit, targetService: string, policyRules?: readonly string[]): Promise<void>;
|
package/dist/biscuit.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// Copyright 2026 Google LLC
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
import { BASELINE_DATALOG } from "./gen/datalog.js";
|
|
15
|
+
import { TaskAuthorizationRuleSchema } from "./gen/agentmesh_pb.js";
|
|
16
|
+
import { loadBiscuitWasm } from "./platform/wasm.js";
|
|
17
|
+
import { effectiveTARExpiration, encodeTARBlockFact, parseTARBlockSource } from "./tar.js";
|
|
18
|
+
let loading;
|
|
19
|
+
/** The biscuit-wasm module, loaded once, the way the runtime loads it. */
|
|
20
|
+
export function loadBiscuit() {
|
|
21
|
+
loading ??= loadBiscuitWasm();
|
|
22
|
+
return loading;
|
|
23
|
+
}
|
|
24
|
+
/** Datalog evaluation budget, as in internal/identity.AuthorizerOptions. */
|
|
25
|
+
export const AUTHORIZER_LIMITS = { max_facts: 1000, max_iterations: 100, max_time_micro: 1_000_000 };
|
|
26
|
+
/**
|
|
27
|
+
* Runs an authorize or query call under AUTHORIZER_LIMITS. biscuit-wasm
|
|
28
|
+
* applies the limits it is passed to the checks and policies only; the
|
|
29
|
+
* fact-generation pass runs under the builder's own limits, 1 ms of wall
|
|
30
|
+
* clock, which it does not let a caller change. A paused event loop or a
|
|
31
|
+
* throttled CPU then answers a valid request with RunLimit Timeout. The
|
|
32
|
+
* facts derived before that deadline stay in the authorizer and the next
|
|
33
|
+
* call resumes from them, so the pass is driven to completion here until
|
|
34
|
+
* the budget above is spent.
|
|
35
|
+
*/
|
|
36
|
+
export function withinLimits(run) {
|
|
37
|
+
const deadline = performance.now() + AUTHORIZER_LIMITS.max_time_micro / 1000;
|
|
38
|
+
for (;;) {
|
|
39
|
+
try {
|
|
40
|
+
return run();
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
if (!isTimeout(err) || performance.now() >= deadline) {
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function isTimeout(err) {
|
|
50
|
+
return typeof err === "object" && err !== null && err.RunLimit === "Timeout";
|
|
51
|
+
}
|
|
52
|
+
export const ROLE_ROUTER = "mesh:role:router";
|
|
53
|
+
export class BiscuitVerificationError extends Error {
|
|
54
|
+
constructor(message, options) {
|
|
55
|
+
super(message, options);
|
|
56
|
+
this.name = "BiscuitVerificationError";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function describe(err) {
|
|
60
|
+
if (err instanceof Error) {
|
|
61
|
+
return err.message;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
return JSON.stringify(err);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return String(err);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function parseWithTrustedKeys(wasm, biscuitBytes, trustedKeys) {
|
|
71
|
+
if (trustedKeys.length === 0) {
|
|
72
|
+
throw new BiscuitVerificationError("no trusted control plane key to verify against");
|
|
73
|
+
}
|
|
74
|
+
let token;
|
|
75
|
+
let verifyingKey;
|
|
76
|
+
let lastErr;
|
|
77
|
+
for (const key of trustedKeys) {
|
|
78
|
+
try {
|
|
79
|
+
token = wasm.Biscuit.fromBytes(biscuitBytes, wasm.PublicKey.fromBytes(key, wasm.SignatureAlgorithm.Ed25519));
|
|
80
|
+
verifyingKey = key;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
lastErr = err;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!token || !verifyingKey) {
|
|
88
|
+
throw new BiscuitVerificationError(`biscuit is not signed by a trusted control plane key: ${describe(lastErr)}`);
|
|
89
|
+
}
|
|
90
|
+
return { token, verifyingKey };
|
|
91
|
+
}
|
|
92
|
+
function extractTARChain(token) {
|
|
93
|
+
const appendedCount = token.countBlocks() - 1;
|
|
94
|
+
if (appendedCount > BASELINE_DATALOG.max_attenuation_blocks) {
|
|
95
|
+
throw new BiscuitVerificationError(`biscuit carries ${appendedCount} appended blocks; maximum is ${BASELINE_DATALOG.max_attenuation_blocks}`);
|
|
96
|
+
}
|
|
97
|
+
const taskRules = [];
|
|
98
|
+
for (let i = 1; i <= appendedCount; i++) {
|
|
99
|
+
try {
|
|
100
|
+
taskRules.push(parseTARBlockSource(token.getBlockSource(i)));
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
throw new BiscuitVerificationError(`biscuit block ${i}: ${describe(err)}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return taskRules;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Verifies a biscuit received from expectedPeerId over an authenticated
|
|
110
|
+
* connection. Every trusted key is tried, so a token minted under a
|
|
111
|
+
* retiring key still verifies during rotation.
|
|
112
|
+
*
|
|
113
|
+
* By default (peer handshakes on /mesh/auth/1.0.0), the authority block must
|
|
114
|
+
* carry node(expectedPeerId). When allowDelegated is true (request tokens in
|
|
115
|
+
* authorizeCaller), the authority block may alternatively carry both
|
|
116
|
+
* actor_node(expectedPeerId) and client_peer_id(expectedPeerId) without node().
|
|
117
|
+
*/
|
|
118
|
+
export async function verifyPeerBiscuit(biscuitBytes, expectedPeerId, trustedKeys, now = new Date(), options) {
|
|
119
|
+
const wasm = await loadBiscuit();
|
|
120
|
+
const { token, verifyingKey } = parseWithTrustedKeys(wasm, biscuitBytes, trustedKeys);
|
|
121
|
+
const taskRules = extractTARChain(token);
|
|
122
|
+
const builder = new wasm.AuthorizerBuilder();
|
|
123
|
+
builder.addFact(wasm.Fact.fromString(`time(${now.toISOString().replace(/\.\d{3}Z$/, "Z")})`));
|
|
124
|
+
builder.addCheck(wasm.Check.fromString("check if time($t), expiration($e), $t <= $e"));
|
|
125
|
+
builder.addPolicy(wasm.Policy.fromString("allow if true"));
|
|
126
|
+
const authorizer = builder.buildAuthenticated(token);
|
|
127
|
+
try {
|
|
128
|
+
withinLimits(() => authorizer.authorizeWithLimits(AUTHORIZER_LIMITS));
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
throw new BiscuitVerificationError(`biscuit is expired or fails its checks: ${describe(err)}`);
|
|
132
|
+
}
|
|
133
|
+
const query = (rule) => withinLimits(() => authorizer.queryWithLimits(wasm.Rule.fromString(rule), AUTHORIZER_LIMITS));
|
|
134
|
+
const strings = (facts) => facts.map((f) => f.terms()[0]).filter((t) => typeof t === "string");
|
|
135
|
+
const bound = strings(query("p($p) <- node($p)"));
|
|
136
|
+
const actorNodes = strings(query("a($a) <- actor_node($a)"));
|
|
137
|
+
const clientPeers = strings(query("c($c) <- client_peer_id($c)"));
|
|
138
|
+
const isBoundNode = bound.includes(expectedPeerId);
|
|
139
|
+
const isBoundDelegated = options?.allowDelegated === true &&
|
|
140
|
+
actorNodes.includes(expectedPeerId) &&
|
|
141
|
+
clientPeers.includes(expectedPeerId);
|
|
142
|
+
if (!isBoundNode && !isBoundDelegated) {
|
|
143
|
+
throw new BiscuitVerificationError(`biscuit is not bound to peer ${expectedPeerId}`);
|
|
144
|
+
}
|
|
145
|
+
const expirations = query("e($e) <- expiration($e)")
|
|
146
|
+
.map((f) => f.terms()[0])
|
|
147
|
+
.filter((t) => t instanceof Date && !Number.isNaN(t.getTime()));
|
|
148
|
+
if (expirations.length === 0) {
|
|
149
|
+
throw new BiscuitVerificationError("biscuit carries no expiration fact");
|
|
150
|
+
}
|
|
151
|
+
const authorityExpiration = new Date(Math.min(...expirations.map((d) => d.getTime())));
|
|
152
|
+
const expiration = effectiveTARExpiration(authorityExpiration, taskRules);
|
|
153
|
+
if (now.getTime() > expiration.getTime()) {
|
|
154
|
+
throw new BiscuitVerificationError(`biscuit is expired at ${now.toISOString()} (effective expiration ${expiration.toISOString()})`);
|
|
155
|
+
}
|
|
156
|
+
const labels = {};
|
|
157
|
+
for (const f of query("l($k, $v) <- label($k, $v)")) {
|
|
158
|
+
const [k, v] = f.terms();
|
|
159
|
+
if (typeof k === "string" && typeof v === "string") {
|
|
160
|
+
labels[k] = v;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
peerId: expectedPeerId,
|
|
165
|
+
...(actorNodes[0] !== undefined ? { actorNode: actorNodes[0] } : {}),
|
|
166
|
+
expiration,
|
|
167
|
+
verifyingKey,
|
|
168
|
+
roles: strings(query("r($r) <- role($r)")),
|
|
169
|
+
labels,
|
|
170
|
+
taskRules,
|
|
171
|
+
token: biscuitBytes,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Appends a non-authority block carrying a single tar_block("<base64url-proto>")
|
|
176
|
+
* fact to an existing Biscuit token in memory without contacting the control plane.
|
|
177
|
+
*/
|
|
178
|
+
export async function attenuateBiscuit(biscuitBytes, ruleInput, trustedKeys) {
|
|
179
|
+
const wasm = await loadBiscuit();
|
|
180
|
+
const { token } = parseWithTrustedKeys(wasm, biscuitBytes, trustedKeys);
|
|
181
|
+
const appendedCount = token.countBlocks() - 1;
|
|
182
|
+
if (appendedCount >= BASELINE_DATALOG.max_attenuation_blocks) {
|
|
183
|
+
throw new BiscuitVerificationError(`biscuit already has ${appendedCount} appended blocks (maximum ${BASELINE_DATALOG.max_attenuation_blocks})`);
|
|
184
|
+
}
|
|
185
|
+
extractTARChain(token);
|
|
186
|
+
const factStr = encodeTARBlockFact(ruleInput);
|
|
187
|
+
const block = wasm.Biscuit.block_builder();
|
|
188
|
+
block.addFact(wasm.Fact.fromString(factStr));
|
|
189
|
+
try {
|
|
190
|
+
return token.appendBlock(block).toBytes();
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
throw new BiscuitVerificationError(`failed to append tar_block to biscuit: ${describe(err)}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Seals a Biscuit token so no further blocks can be appended by downstream holders.
|
|
198
|
+
*/
|
|
199
|
+
export async function sealBiscuit(biscuitBytes, trustedKeys) {
|
|
200
|
+
const wasm = await loadBiscuit();
|
|
201
|
+
const { token } = parseWithTrustedKeys(wasm, biscuitBytes, trustedKeys);
|
|
202
|
+
extractTARChain(token);
|
|
203
|
+
try {
|
|
204
|
+
return token.sealToken().toBytes();
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
throw new BiscuitVerificationError(`failed to seal biscuit: ${describe(err)}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/** Requires role(<role>) on an already verified token, as identity.RequireRole. */
|
|
211
|
+
export function requireRole(verified, role) {
|
|
212
|
+
if (!verified.roles.includes(role)) {
|
|
213
|
+
throw new BiscuitVerificationError(`biscuit lacks expected role ${JSON.stringify(role)}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Requires that an already verified provider credential grants serving
|
|
218
|
+
* targetService ("<type>://<name>"), as identity.RequirePublish: the
|
|
219
|
+
* directory names whoever announced the service, only the credential says
|
|
220
|
+
* who may serve it. The provider's own catalog names no service and needs
|
|
221
|
+
* no grant; so does an empty target.
|
|
222
|
+
*/
|
|
223
|
+
export async function requirePublish(verified, targetService, policyRules = []) {
|
|
224
|
+
if (targetService === "" || targetService === `system://${BASELINE_DATALOG.catalog_target}`) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const m = /^([a-z0-9]+):\/\/(.+)$/.exec(targetService);
|
|
228
|
+
if (m === null) {
|
|
229
|
+
throw new BiscuitVerificationError(`service ${JSON.stringify(targetService)} must be <type>://<name>`);
|
|
230
|
+
}
|
|
231
|
+
const wasm = await loadBiscuit();
|
|
232
|
+
const token = wasm.Biscuit.fromBytes(verified.token, wasm.PublicKey.fromBytes(verified.verifyingKey, wasm.SignatureAlgorithm.Ed25519));
|
|
233
|
+
const builder = new wasm.AuthorizerBuilder();
|
|
234
|
+
builder.addFact(wasm.Fact.fromString(`${BASELINE_DATALOG.fact_publish}(${JSON.stringify(m[1])}, ${JSON.stringify(m[2])})`));
|
|
235
|
+
// The mesh policy derives grants from the credential's role() and label()
|
|
236
|
+
// facts, so a policy change reaches this check before the provider's
|
|
237
|
+
// credential is minted again.
|
|
238
|
+
for (const r of policyRules) {
|
|
239
|
+
builder.addRule(wasm.Rule.fromString(r));
|
|
240
|
+
}
|
|
241
|
+
for (const policy of BASELINE_DATALOG.publish_policies) {
|
|
242
|
+
builder.addPolicy(wasm.Policy.fromString(policy));
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
withinLimits(() => builder.buildAuthenticated(token).authorizeWithLimits(AUTHORIZER_LIMITS));
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
throw new BiscuitVerificationError(`peer ${verified.peerId} is not granted serving ${targetService}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
package/dist/bytes.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function toHex(bytes: Uint8Array): string;
|
|
2
|
+
export declare function fromHex(text: string): Uint8Array;
|
|
3
|
+
/** Padded standard base64 (RFC 4648 section 4), what Go base64.StdEncoding decodes. */
|
|
4
|
+
export declare function toBase64(bytes: Uint8Array): string;
|
|
5
|
+
export declare function fromBase64(text: string): Uint8Array;
|
|
6
|
+
/** Unpadded base64url (RFC 4648 section 5), what Go base64.RawURLEncoding decodes. */
|
|
7
|
+
export declare function toBase64Url(bytes: Uint8Array): string;
|
|
8
|
+
export declare function fromBase64Url(text: string): Uint8Array;
|
|
9
|
+
export declare function bytesEqual(a: Uint8Array, b: Uint8Array): boolean;
|
|
10
|
+
export declare function concatBytes(...parts: Uint8Array[]): Uint8Array<ArrayBuffer>;
|
package/dist/bytes.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Copyright 2026 Google LLC
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
// Text encodings of bytes as the Go side reads them, for any JavaScript
|
|
15
|
+
// runtime. The names say which alphabet and whether the text is padded:
|
|
16
|
+
// a biscuit travels as padded standard base64 (Go base64.StdEncoding), a
|
|
17
|
+
// challenge signature as unpadded base64url (Go base64.RawURLEncoding).
|
|
18
|
+
import { fromString } from "uint8arrays/from-string";
|
|
19
|
+
import { toString } from "uint8arrays/to-string";
|
|
20
|
+
export function toHex(bytes) {
|
|
21
|
+
return toString(bytes, "hex");
|
|
22
|
+
}
|
|
23
|
+
export function fromHex(text) {
|
|
24
|
+
return fromString(text, "hex");
|
|
25
|
+
}
|
|
26
|
+
/** Padded standard base64 (RFC 4648 section 4), what Go base64.StdEncoding decodes. */
|
|
27
|
+
export function toBase64(bytes) {
|
|
28
|
+
return toString(bytes, "base64pad");
|
|
29
|
+
}
|
|
30
|
+
export function fromBase64(text) {
|
|
31
|
+
return fromString(text, "base64pad");
|
|
32
|
+
}
|
|
33
|
+
/** Unpadded base64url (RFC 4648 section 5), what Go base64.RawURLEncoding decodes. */
|
|
34
|
+
export function toBase64Url(bytes) {
|
|
35
|
+
return toString(bytes, "base64url");
|
|
36
|
+
}
|
|
37
|
+
export function fromBase64Url(text) {
|
|
38
|
+
return fromString(text, "base64url");
|
|
39
|
+
}
|
|
40
|
+
export function bytesEqual(a, b) {
|
|
41
|
+
return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
42
|
+
}
|
|
43
|
+
export function concatBytes(...parts) {
|
|
44
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
45
|
+
let offset = 0;
|
|
46
|
+
for (const p of parts) {
|
|
47
|
+
out.set(p, offset);
|
|
48
|
+
offset += p.length;
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Signed at POST /enroll (bootstrap token enrollment). */
|
|
2
|
+
export declare function enrollChallenge(peerId: string, ts: number): Uint8Array;
|
|
3
|
+
/** Signed at GET /enroll/status while a bootstrap enrollment is pending. */
|
|
4
|
+
export declare function enrollStatusChallenge(peerId: string, ts: number): Uint8Array;
|
|
5
|
+
/** Signed at POST /register (OIDC enrollment). */
|
|
6
|
+
export declare function registerChallenge(peerId: string, ts: number): Uint8Array;
|
|
7
|
+
/** Signed at POST /refresh. */
|
|
8
|
+
export declare function refreshChallenge(peerId: string, ts: number): Uint8Array;
|
|
9
|
+
/** Signed at GET /policies. */
|
|
10
|
+
export declare function policiesChallenge(peerId: string, ts: number): Uint8Array;
|
|
11
|
+
/** Signed at GET /egress. */
|
|
12
|
+
export declare function egressChallenge(peerId: string, ts: number): Uint8Array;
|
|
13
|
+
/** Signed at GET /revocations. */
|
|
14
|
+
export declare function revocationsChallenge(peerId: string, ts: number): Uint8Array;
|
|
15
|
+
/** Signed at POST /nodes/catalog. */
|
|
16
|
+
export declare function nodesCatalogChallenge(peerId: string, ts: number): Uint8Array;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Copyright 2026 Google LLC
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
// Proof-of-possession challenges on the control plane's mesh surface.
|
|
15
|
+
// Each payload names the peer and the endpoint, so a captured signature
|
|
16
|
+
// verifies nowhere else. Mirrors api/network.go; ts is unix milliseconds.
|
|
17
|
+
const encoder = new TextEncoder();
|
|
18
|
+
function challenge(domain, peerId, ts) {
|
|
19
|
+
if (!Number.isSafeInteger(ts) || ts <= 0) {
|
|
20
|
+
throw new Error(`challenge timestamp must be a positive integer, got ${ts}`);
|
|
21
|
+
}
|
|
22
|
+
return encoder.encode(`mesh:${domain}:${peerId}:${ts}`);
|
|
23
|
+
}
|
|
24
|
+
/** Signed at POST /enroll (bootstrap token enrollment). */
|
|
25
|
+
export function enrollChallenge(peerId, ts) {
|
|
26
|
+
return challenge("enroll", peerId, ts);
|
|
27
|
+
}
|
|
28
|
+
/** Signed at GET /enroll/status while a bootstrap enrollment is pending. */
|
|
29
|
+
export function enrollStatusChallenge(peerId, ts) {
|
|
30
|
+
return challenge("enroll-status", peerId, ts);
|
|
31
|
+
}
|
|
32
|
+
/** Signed at POST /register (OIDC enrollment). */
|
|
33
|
+
export function registerChallenge(peerId, ts) {
|
|
34
|
+
return challenge("register", peerId, ts);
|
|
35
|
+
}
|
|
36
|
+
/** Signed at POST /refresh. */
|
|
37
|
+
export function refreshChallenge(peerId, ts) {
|
|
38
|
+
return challenge("refresh", peerId, ts);
|
|
39
|
+
}
|
|
40
|
+
/** Signed at GET /policies. */
|
|
41
|
+
export function policiesChallenge(peerId, ts) {
|
|
42
|
+
return challenge("policies", peerId, ts);
|
|
43
|
+
}
|
|
44
|
+
/** Signed at GET /egress. */
|
|
45
|
+
export function egressChallenge(peerId, ts) {
|
|
46
|
+
return challenge("egress", peerId, ts);
|
|
47
|
+
}
|
|
48
|
+
/** Signed at GET /revocations. */
|
|
49
|
+
export function revocationsChallenge(peerId, ts) {
|
|
50
|
+
return challenge("revocations", peerId, ts);
|
|
51
|
+
}
|
|
52
|
+
/** Signed at POST /nodes/catalog. */
|
|
53
|
+
export function nodesCatalogChallenge(peerId, ts) {
|
|
54
|
+
return challenge("nodes-catalog", peerId, ts);
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|