@forestrie/mcp-register 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/LICENSE +21 -0
- package/README.md +126 -2
- package/bin/mcp-register.mjs +2 -0
- package/dist/core/base64.d.ts +7 -0
- package/dist/core/base64.d.ts.map +1 -0
- package/dist/core/base64.js +20 -0
- package/dist/core/base64.js.map +1 -0
- package/dist/core/delegation.d.ts +56 -0
- package/dist/core/delegation.d.ts.map +1 -0
- package/dist/core/delegation.js +130 -0
- package/dist/core/delegation.js.map +1 -0
- package/dist/core/grant.d.ts +41 -0
- package/dist/core/grant.d.ts.map +1 -0
- package/dist/core/grant.js +78 -0
- package/dist/core/grant.js.map +1 -0
- package/dist/core/hex.d.ts +4 -0
- package/dist/core/hex.d.ts.map +1 -0
- package/dist/core/hex.js +18 -0
- package/dist/core/hex.js.map +1 -0
- package/dist/core/index.d.ts +28 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +21 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/issue.d.ts +38 -0
- package/dist/core/issue.d.ts.map +1 -0
- package/dist/core/issue.js +135 -0
- package/dist/core/issue.js.map +1 -0
- package/dist/core/limits.d.ts +9 -0
- package/dist/core/limits.d.ts.map +1 -0
- package/dist/core/limits.js +9 -0
- package/dist/core/limits.js.map +1 -0
- package/dist/core/scrapi.d.ts +66 -0
- package/dist/core/scrapi.d.ts.map +1 -0
- package/dist/core/scrapi.js +174 -0
- package/dist/core/scrapi.js.map +1 -0
- package/dist/core/signer.d.ts +24 -0
- package/dist/core/signer.d.ts.map +1 -0
- package/dist/core/signer.js +2 -0
- package/dist/core/signer.js.map +1 -0
- package/dist/core/statement.d.ts +26 -0
- package/dist/core/statement.d.ts.map +1 -0
- package/dist/core/statement.js +70 -0
- package/dist/core/statement.js.map +1 -0
- package/dist/core/version.d.ts +23 -0
- package/dist/core/version.d.ts.map +1 -0
- package/dist/core/version.js +23 -0
- package/dist/core/version.js.map +1 -0
- package/dist/core/x402.d.ts +99 -0
- package/dist/core/x402.d.ts.map +1 -0
- package/dist/core/x402.js +202 -0
- package/dist/core/x402.js.map +1 -0
- package/dist/net/delegate.d.ts +56 -0
- package/dist/net/delegate.d.ts.map +1 -0
- package/dist/net/delegate.js +182 -0
- package/dist/net/delegate.js.map +1 -0
- package/dist/net/http.d.ts +17 -0
- package/dist/net/http.d.ts.map +1 -0
- package/dist/net/http.js +52 -0
- package/dist/net/http.js.map +1 -0
- package/dist/net/index.d.ts +14 -0
- package/dist/net/index.d.ts.map +1 -0
- package/dist/net/index.js +13 -0
- package/dist/net/index.js.map +1 -0
- package/dist/net/write.d.ts +55 -0
- package/dist/net/write.d.ts.map +1 -0
- package/dist/net/write.js +46 -0
- package/dist/net/write.js.map +1 -0
- package/dist/node/cli.d.ts +8 -0
- package/dist/node/cli.d.ts.map +1 -0
- package/dist/node/cli.js +143 -0
- package/dist/node/cli.js.map +1 -0
- package/dist/node/grant-store.d.ts +49 -0
- package/dist/node/grant-store.d.ts.map +1 -0
- package/dist/node/grant-store.js +95 -0
- package/dist/node/grant-store.js.map +1 -0
- package/dist/node/grant-tools.d.ts +29 -0
- package/dist/node/grant-tools.d.ts.map +1 -0
- package/dist/node/grant-tools.js +498 -0
- package/dist/node/grant-tools.js.map +1 -0
- package/dist/node/ledger.d.ts +26 -0
- package/dist/node/ledger.d.ts.map +1 -0
- package/dist/node/ledger.js +78 -0
- package/dist/node/ledger.js.map +1 -0
- package/dist/node/payer.d.ts +48 -0
- package/dist/node/payer.d.ts.map +1 -0
- package/dist/node/payer.js +170 -0
- package/dist/node/payer.js.map +1 -0
- package/dist/node/resolve-input.d.ts +19 -0
- package/dist/node/resolve-input.d.ts.map +1 -0
- package/dist/node/resolve-input.js +76 -0
- package/dist/node/resolve-input.js.map +1 -0
- package/dist/node/server.d.ts +47 -0
- package/dist/node/server.d.ts.map +1 -0
- package/dist/node/server.js +212 -0
- package/dist/node/server.js.map +1 -0
- package/dist/node/signer-file.d.ts +34 -0
- package/dist/node/signer-file.d.ts.map +1 -0
- package/dist/node/signer-file.js +183 -0
- package/dist/node/signer-file.js.map +1 -0
- package/dist/node/store.d.ts +40 -0
- package/dist/node/store.d.ts.map +1 -0
- package/dist/node/store.js +117 -0
- package/dist/node/store.js.map +1 -0
- package/dist/node/text.d.ts +29 -0
- package/dist/node/text.d.ts.map +1 -0
- package/dist/node/text.js +124 -0
- package/dist/node/text.js.map +1 -0
- package/dist/node/tools.d.ts +118 -0
- package/dist/node/tools.d.ts.map +1 -0
- package/dist/node/tools.js +548 -0
- package/dist/node/tools.js.map +1 -0
- package/package.json +108 -3
- package/src/core/base64.ts +18 -0
- package/src/core/delegation.ts +239 -0
- package/src/core/grant.ts +127 -0
- package/src/core/hex.ts +18 -0
- package/src/core/index.ts +84 -0
- package/src/core/issue.ts +209 -0
- package/src/core/limits.ts +8 -0
- package/src/core/scrapi.ts +236 -0
- package/src/core/signer.ts +23 -0
- package/src/core/statement.ts +133 -0
- package/src/core/version.ts +25 -0
- package/src/core/x402.ts +314 -0
- package/src/net/delegate.ts +274 -0
- package/src/net/http.ts +71 -0
- package/src/net/index.ts +26 -0
- package/src/net/write.ts +124 -0
- package/src/node/cli.ts +170 -0
- package/src/node/grant-store.ts +154 -0
- package/src/node/grant-tools.ts +707 -0
- package/src/node/ledger.ts +98 -0
- package/src/node/payer.ts +241 -0
- package/src/node/resolve-input.ts +96 -0
- package/src/node/server.ts +332 -0
- package/src/node/signer-file.ts +251 -0
- package/src/node/store.ts +175 -0
- package/src/node/text.ts +153 -0
- package/src/node/tools.ts +810 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type GrantView } from "../core/grant.js";
|
|
3
|
+
import { type Problem } from "../core/scrapi.js";
|
|
4
|
+
import type { Es256Signer } from "../core/signer.js";
|
|
5
|
+
import type { GrantStore } from "./grant-store.js";
|
|
6
|
+
import type { SpendLedger } from "./ledger.js";
|
|
7
|
+
import { type PayerBackend } from "./payer.js";
|
|
8
|
+
import { type BytesInput } from "./resolve-input.js";
|
|
9
|
+
import { RegistrationStore } from "./store.js";
|
|
10
|
+
import type { ToolName } from "./text.js";
|
|
11
|
+
export interface ResolvedDeps {
|
|
12
|
+
fetchImpl: typeof fetch;
|
|
13
|
+
env: Record<string, string | undefined>;
|
|
14
|
+
now: () => Date;
|
|
15
|
+
/** Opened on first use, so starting the server writes no file. With
|
|
16
|
+
* `create: false` a missing key is an error rather than a new key. */
|
|
17
|
+
signer: (options?: {
|
|
18
|
+
create?: boolean;
|
|
19
|
+
}) => Promise<Es256Signer & {
|
|
20
|
+
created?: boolean;
|
|
21
|
+
}>;
|
|
22
|
+
/** The requester's authority key, which signs child grants: a separate
|
|
23
|
+
* key, opened the same way as the log key. */
|
|
24
|
+
authoritySigner: (options?: {
|
|
25
|
+
create?: boolean;
|
|
26
|
+
}) => Promise<Es256Signer & {
|
|
27
|
+
created?: boolean;
|
|
28
|
+
}>;
|
|
29
|
+
/** The configured payer, or `undefined` for relay mode. Throws
|
|
30
|
+
* `PayerError` when one is configured but cannot be opened. */
|
|
31
|
+
payer: () => PayerBackend | undefined;
|
|
32
|
+
/** Paths no `{path}` input may read: the key files and the server's home. */
|
|
33
|
+
protectedPaths: string[];
|
|
34
|
+
store: RegistrationStore;
|
|
35
|
+
grants: GrantStore;
|
|
36
|
+
ledger: SpendLedger;
|
|
37
|
+
sleep?: (ms: number) => Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
export declare const GrantSchema: z.ZodString;
|
|
40
|
+
export declare const BaseUrlSchema: z.ZodString;
|
|
41
|
+
export declare const getLogKeyInputShape: {
|
|
42
|
+
which: z.ZodOptional<z.ZodEnum<{
|
|
43
|
+
log: "log";
|
|
44
|
+
authority: "authority";
|
|
45
|
+
}>>;
|
|
46
|
+
};
|
|
47
|
+
export declare const registerStatementInputShape: {
|
|
48
|
+
payload: z.ZodUnion<readonly [z.ZodObject<{
|
|
49
|
+
b64: z.ZodString;
|
|
50
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
51
|
+
base64: z.ZodString;
|
|
52
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
53
|
+
path: z.ZodString;
|
|
54
|
+
}, z.core.$strip>]>;
|
|
55
|
+
contentType: z.ZodString;
|
|
56
|
+
grant: z.ZodOptional<z.ZodString>;
|
|
57
|
+
baseUrl: z.ZodOptional<z.ZodString>;
|
|
58
|
+
rootLogId: z.ZodOptional<z.ZodString>;
|
|
59
|
+
subject: z.ZodOptional<z.ZodString>;
|
|
60
|
+
issuer: z.ZodOptional<z.ZodString>;
|
|
61
|
+
};
|
|
62
|
+
export declare const checkRegistrationInputShape: {
|
|
63
|
+
contentHash: z.ZodString;
|
|
64
|
+
};
|
|
65
|
+
export declare const verifyRegistrationInputShape: {
|
|
66
|
+
contentHash: z.ZodString;
|
|
67
|
+
knownAccumulator: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
68
|
+
b64: z.ZodString;
|
|
69
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
70
|
+
base64: z.ZodString;
|
|
71
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
72
|
+
path: z.ZodString;
|
|
73
|
+
}, z.core.$strip>]>>;
|
|
74
|
+
};
|
|
75
|
+
export declare const delegateSealingInputShape: {
|
|
76
|
+
grant: z.ZodOptional<z.ZodString>;
|
|
77
|
+
baseUrl: z.ZodOptional<z.ZodString>;
|
|
78
|
+
coordinatorUrl: z.ZodOptional<z.ZodString>;
|
|
79
|
+
knownSealerKey: z.ZodOptional<z.ZodString>;
|
|
80
|
+
ttlSeconds: z.ZodOptional<z.ZodNumber>;
|
|
81
|
+
as: z.ZodOptional<z.ZodEnum<{
|
|
82
|
+
log: "log";
|
|
83
|
+
authority: "authority";
|
|
84
|
+
}>>;
|
|
85
|
+
};
|
|
86
|
+
export type Args<S extends Record<string, z.ZodType>> = {
|
|
87
|
+
[K in keyof S]: z.infer<S[K]>;
|
|
88
|
+
};
|
|
89
|
+
export type ToolResult = {
|
|
90
|
+
content: [{
|
|
91
|
+
type: "text";
|
|
92
|
+
text: string;
|
|
93
|
+
}];
|
|
94
|
+
structuredContent: Record<string, unknown>;
|
|
95
|
+
isError: false;
|
|
96
|
+
};
|
|
97
|
+
export declare function ok(text: string, structuredContent: Record<string, unknown>): ToolResult;
|
|
98
|
+
export declare function problemResult(toolName: ToolName, problem: Record<string, unknown>): ToolResult;
|
|
99
|
+
export declare function httpProblemValue(message: string, p: Problem): Record<string, unknown>;
|
|
100
|
+
export declare function httpProblem(toolName: ToolName, message: string, p: Problem): ToolResult;
|
|
101
|
+
export declare function missingInput(toolName: ToolName, field: string, envVar: string): ToolResult;
|
|
102
|
+
export declare function guardHandler(toolName: ToolName, run: () => Promise<ToolResult>): Promise<ToolResult>;
|
|
103
|
+
export interface ResolvedGrant {
|
|
104
|
+
b64: string;
|
|
105
|
+
view: GrantView;
|
|
106
|
+
}
|
|
107
|
+
/** `resolveBytes`, with `{path}` inputs kept off the key and the home. */
|
|
108
|
+
export declare function readBytes(input: BytesInput, what: string, deps: ResolvedDeps): Uint8Array<ArrayBufferLike>;
|
|
109
|
+
/** A grant from a tool argument or FORESTRIE_GRANT: inline base64, or a path
|
|
110
|
+
* to a file of raw COSE bytes or of base64 text. */
|
|
111
|
+
export declare function resolveGrant(explicit: string | undefined, deps: ResolvedDeps, envVar?: string): ResolvedGrant | undefined;
|
|
112
|
+
export declare function decodeBase64Text(text: string, what: string): Uint8Array;
|
|
113
|
+
export declare function makeGetLogKeyTool(deps: ResolvedDeps): (args?: Partial<Args<typeof getLogKeyInputShape>>) => Promise<ToolResult>;
|
|
114
|
+
export declare function makeRegisterStatementTool(deps: ResolvedDeps): (args: Args<typeof registerStatementInputShape>) => Promise<ToolResult>;
|
|
115
|
+
export declare function makeCheckRegistrationTool(deps: ResolvedDeps): (args: Args<typeof checkRegistrationInputShape>) => Promise<ToolResult>;
|
|
116
|
+
export declare function makeVerifyRegistrationTool(deps: ResolvedDeps): (args: Args<typeof verifyRegistrationInputShape>) => Promise<ToolResult>;
|
|
117
|
+
export declare function makeDelegateSealingTool(deps: ResolvedDeps): (args: Args<typeof delegateSealingInputShape>) => Promise<ToolResult>;
|
|
118
|
+
//# sourceMappingURL=tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/node/tools.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tool handlers. Each returns `{ content, structuredContent, isError:
|
|
3
|
+
* false }`; a refusal, an HTTP failure or bad input is a
|
|
4
|
+
* `structuredContent.problem` with a `code`, never a throw. Only programming
|
|
5
|
+
* errors propagate.
|
|
6
|
+
*
|
|
7
|
+
* Nothing here returns key material or accepts a signature: the signer is
|
|
8
|
+
* reached only through `deps.signer()`.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
11
|
+
import { isAbsolute, relative } from "node:path";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { NetError, fetchReceipt } from "@forestrie/mcp-resolve/net";
|
|
14
|
+
import { EndpointError, registrationStatusUrl, summarizeFetched, verifyFetched, } from "@forestrie/mcp-resolve";
|
|
15
|
+
import { GrantError, inspectGrant, statementGrantProblems, } from "../core/grant.js";
|
|
16
|
+
import { base64ToBytes, bytesToBase64 } from "../core/base64.js";
|
|
17
|
+
import { toHex } from "../core/hex.js";
|
|
18
|
+
import { MAX_STATEMENT_BYTES } from "../core/limits.js";
|
|
19
|
+
import { readReceiptResponse, readRegisterResponse, readStatusResponse, originOf, } from "../core/scrapi.js";
|
|
20
|
+
import { StatementError, signStatement } from "../core/statement.js";
|
|
21
|
+
import { delegateSealing } from "../net/delegate.js";
|
|
22
|
+
import { getRegistrationStatus, postStatement } from "../net/write.js";
|
|
23
|
+
import { PaymentError } from "../core/x402.js";
|
|
24
|
+
import { PayerError } from "./payer.js";
|
|
25
|
+
import { InputError, resolveBytes } from "./resolve-input.js";
|
|
26
|
+
import { KeyFileError, publicKeyPem } from "./signer-file.js";
|
|
27
|
+
import { RegistrationStore, StoreError, } from "./store.js";
|
|
28
|
+
/* -------------------------------- deps ---------------------------------- */
|
|
29
|
+
/** How long check_registration reports "pending" before warning that
|
|
30
|
+
* canopy may never have received the statement. */
|
|
31
|
+
const STALE_PENDING_MINUTES = 10;
|
|
32
|
+
/* ------------------------------- schemas -------------------------------- */
|
|
33
|
+
const BYTES_SHAPES = "{ b64 } | { base64 } | { path }";
|
|
34
|
+
const BytesInputSchema = z
|
|
35
|
+
.union([
|
|
36
|
+
z.object({ b64: z.string().min(1) }).describe("standard base64"),
|
|
37
|
+
z
|
|
38
|
+
.object({ base64: z.string().min(1) })
|
|
39
|
+
.describe("standard base64 (alias of b64)"),
|
|
40
|
+
z
|
|
41
|
+
.object({ path: z.string().min(1) })
|
|
42
|
+
.describe("filesystem path, read by the stdio adapter"),
|
|
43
|
+
], { error: `expected bytes as ${BYTES_SHAPES}` })
|
|
44
|
+
.describe(`Bytes as ${BYTES_SHAPES}`);
|
|
45
|
+
function bytesInput(what) {
|
|
46
|
+
return BytesInputSchema.describe(`${what} — ${BYTES_SHAPES}`);
|
|
47
|
+
}
|
|
48
|
+
export const GrantSchema = z
|
|
49
|
+
.string()
|
|
50
|
+
.min(1)
|
|
51
|
+
.describe("the Forestrie-Grant: base64 (standard or URL-safe) of the completed grant, or a path to a file holding it as raw bytes or base64 text; falls back to FORESTRIE_GRANT");
|
|
52
|
+
export const BaseUrlSchema = z
|
|
53
|
+
.string()
|
|
54
|
+
.min(1)
|
|
55
|
+
.describe("the canopy SCRAPI base URL; falls back to FORESTRIE_BASE_URL — never defaulted by the package itself");
|
|
56
|
+
const ContentHashSchema = z
|
|
57
|
+
.string()
|
|
58
|
+
.regex(/^[0-9a-f]{64}$/)
|
|
59
|
+
.describe("the content hash register_statement returned (64 lowercase hex)");
|
|
60
|
+
const KeyRoleSchema = z
|
|
61
|
+
.enum(["log", "authority"])
|
|
62
|
+
.optional()
|
|
63
|
+
.describe('which key: "log" (default), the key that owns your data log and signs statements; or "authority", the requester\'s key that owns an authority log and signs the child grants issue_grant makes');
|
|
64
|
+
export const getLogKeyInputShape = {
|
|
65
|
+
which: KeyRoleSchema,
|
|
66
|
+
};
|
|
67
|
+
function signerFor(deps, role) {
|
|
68
|
+
return role === "authority" ? deps.authoritySigner : deps.signer;
|
|
69
|
+
}
|
|
70
|
+
export const registerStatementInputShape = {
|
|
71
|
+
payload: bytesInput("the payload to sign and register, up to 4 MiB"),
|
|
72
|
+
contentType: z
|
|
73
|
+
.string()
|
|
74
|
+
.min(1)
|
|
75
|
+
.describe("the payload's media type, e.g. application/json"),
|
|
76
|
+
grant: GrantSchema.optional(),
|
|
77
|
+
baseUrl: BaseUrlSchema.optional(),
|
|
78
|
+
rootLogId: z
|
|
79
|
+
.string()
|
|
80
|
+
.min(1)
|
|
81
|
+
.optional()
|
|
82
|
+
.describe("the forest's root log id (R in /register/{R}/entries); falls back to FORESTRIE_ROOT_LOG_ID"),
|
|
83
|
+
subject: z
|
|
84
|
+
.string()
|
|
85
|
+
.min(1)
|
|
86
|
+
.optional()
|
|
87
|
+
.describe("CWT sub; defaults to sha-256:<hex of the payload>"),
|
|
88
|
+
issuer: z
|
|
89
|
+
.string()
|
|
90
|
+
.min(1)
|
|
91
|
+
.optional()
|
|
92
|
+
.describe("CWT iss; defaults to the hex of the key id"),
|
|
93
|
+
};
|
|
94
|
+
export const checkRegistrationInputShape = {
|
|
95
|
+
contentHash: ContentHashSchema,
|
|
96
|
+
};
|
|
97
|
+
export const verifyRegistrationInputShape = {
|
|
98
|
+
contentHash: ContentHashSchema,
|
|
99
|
+
knownAccumulator: bytesInput("optional: a known accumulator (e.g. from @forestrie/mcp-resolve's fetch_accumulator) to verify against instead of this server's own log key").optional(),
|
|
100
|
+
};
|
|
101
|
+
export const delegateSealingInputShape = {
|
|
102
|
+
grant: GrantSchema.optional(),
|
|
103
|
+
baseUrl: BaseUrlSchema.optional(),
|
|
104
|
+
coordinatorUrl: z
|
|
105
|
+
.string()
|
|
106
|
+
.min(1)
|
|
107
|
+
.optional()
|
|
108
|
+
.describe("the delegation coordinator base URL; falls back to FORESTRIE_COORDINATOR_URL"),
|
|
109
|
+
knownSealerKey: z
|
|
110
|
+
.string()
|
|
111
|
+
.min(1)
|
|
112
|
+
.optional()
|
|
113
|
+
.describe("the registrar's voucher key, base64 of 64 bytes x‖y, as the operator publishes it; falls back to FORESTRIE_KNOWN_SEALER_KEY"),
|
|
114
|
+
ttlSeconds: z
|
|
115
|
+
.number()
|
|
116
|
+
.int()
|
|
117
|
+
.positive()
|
|
118
|
+
.optional()
|
|
119
|
+
.describe("how long the delegation lasts; defaults to the coordinator's suggestion"),
|
|
120
|
+
as: z
|
|
121
|
+
.enum(["log", "authority"])
|
|
122
|
+
.optional()
|
|
123
|
+
.describe('which key owns the log: "log" (default) for your data log, under the grant you register statements with; "authority" for your authority log, under its creation grant (FORESTRIE_AUTHORITY_GRANT), so the child grants register_grant sequences there get sealed'),
|
|
124
|
+
};
|
|
125
|
+
export function ok(text, structuredContent) {
|
|
126
|
+
return {
|
|
127
|
+
content: [{ type: "text", text }],
|
|
128
|
+
structuredContent,
|
|
129
|
+
isError: false,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
export function problemResult(toolName, problem) {
|
|
133
|
+
const message = typeof problem["message"] === "string"
|
|
134
|
+
? problem["message"]
|
|
135
|
+
: JSON.stringify(problem);
|
|
136
|
+
const code = typeof problem["code"] === "string" ? problem["code"] : "problem";
|
|
137
|
+
return ok(`${toolName}: problem (${code}): ${message}`, { problem });
|
|
138
|
+
}
|
|
139
|
+
export function httpProblemValue(message, p) {
|
|
140
|
+
return {
|
|
141
|
+
code: "http_error",
|
|
142
|
+
message: `${message}: ${p.title}${p.detail ? ` — ${p.detail}` : ""}${p.retryAfterSeconds !== undefined ? ` (retry after ${p.retryAfterSeconds}s)` : ""}`,
|
|
143
|
+
status: p.status,
|
|
144
|
+
title: p.title,
|
|
145
|
+
...(p.detail !== undefined ? { detail: p.detail } : {}),
|
|
146
|
+
...(p.reason !== undefined ? { reason: p.reason } : {}),
|
|
147
|
+
...(p.retryAfterSeconds !== undefined
|
|
148
|
+
? { retryAfterSeconds: p.retryAfterSeconds }
|
|
149
|
+
: {}),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
export function httpProblem(toolName, message, p) {
|
|
153
|
+
return problemResult(toolName, httpProblemValue(message, p));
|
|
154
|
+
}
|
|
155
|
+
export function missingInput(toolName, field, envVar) {
|
|
156
|
+
return problemResult(toolName, {
|
|
157
|
+
code: "missing_input",
|
|
158
|
+
message: `${field} is required: pass it explicitly or set ${envVar}`,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
export async function guardHandler(toolName, run) {
|
|
162
|
+
try {
|
|
163
|
+
return await run();
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
if (err instanceof InputError ||
|
|
167
|
+
err instanceof GrantError ||
|
|
168
|
+
err instanceof StoreError ||
|
|
169
|
+
err instanceof KeyFileError ||
|
|
170
|
+
err instanceof StatementError ||
|
|
171
|
+
err instanceof NetError ||
|
|
172
|
+
err instanceof PaymentError ||
|
|
173
|
+
err instanceof PayerError) {
|
|
174
|
+
return problemResult(toolName, { code: err.code, message: err.message });
|
|
175
|
+
}
|
|
176
|
+
if (err instanceof EndpointError)
|
|
177
|
+
return problemResult(toolName, {
|
|
178
|
+
code: "invalid_input",
|
|
179
|
+
message: err.message,
|
|
180
|
+
});
|
|
181
|
+
// Any filesystem failure not mapped above (a Node errno error).
|
|
182
|
+
if (err instanceof Error &&
|
|
183
|
+
typeof err.syscall === "string")
|
|
184
|
+
return problemResult(toolName, {
|
|
185
|
+
code: "io_error",
|
|
186
|
+
message: err.message,
|
|
187
|
+
});
|
|
188
|
+
throw err;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Refuse a `{path}` that resolves to the log key file or anywhere under the
|
|
193
|
+
* server's home: a prompt-injected caller must not be able to get the
|
|
194
|
+
* private key signed and registered on a public log.
|
|
195
|
+
*/
|
|
196
|
+
function assertReadable(path, what, deps) {
|
|
197
|
+
let real;
|
|
198
|
+
try {
|
|
199
|
+
real = realpathSync(path);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return; // missing or unreadable: the read itself reports it
|
|
203
|
+
}
|
|
204
|
+
for (const guarded of deps.protectedPaths) {
|
|
205
|
+
let g;
|
|
206
|
+
try {
|
|
207
|
+
g = realpathSync(guarded);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const rel = relative(g, real);
|
|
213
|
+
if (rel === "" || (!rel.startsWith("..") && !isAbsolute(rel)))
|
|
214
|
+
throw new InputError(`${what} may not read ${path}: it is the log key or inside this server's own home`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/** `resolveBytes`, with `{path}` inputs kept off the key and the home. */
|
|
218
|
+
export function readBytes(input, what, deps) {
|
|
219
|
+
if ("path" in input)
|
|
220
|
+
assertReadable(input.path, `${what}.path`, deps);
|
|
221
|
+
return resolveBytes(input, what);
|
|
222
|
+
}
|
|
223
|
+
/** A grant from a tool argument or FORESTRIE_GRANT: inline base64, or a path
|
|
224
|
+
* to a file of raw COSE bytes or of base64 text. */
|
|
225
|
+
export function resolveGrant(explicit, deps, envVar = "FORESTRIE_GRANT") {
|
|
226
|
+
const raw = explicit ?? deps.env[envVar];
|
|
227
|
+
if (raw === undefined || raw.trim() === "")
|
|
228
|
+
return undefined;
|
|
229
|
+
let bytes;
|
|
230
|
+
if (existsSync(raw)) {
|
|
231
|
+
assertReadable(raw, "grant", deps);
|
|
232
|
+
if (!statSync(raw).isFile())
|
|
233
|
+
throw new InputError(`grant path ${raw} is not a file`);
|
|
234
|
+
let file;
|
|
235
|
+
try {
|
|
236
|
+
file = new Uint8Array(readFileSync(raw));
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
throw new InputError(`cannot read grant file ${raw}: ${err.message}`);
|
|
240
|
+
}
|
|
241
|
+
// A COSE Sign1 starts 0x84 (untagged) or 0xd2 (tag 18); anything else is
|
|
242
|
+
// read as base64 text.
|
|
243
|
+
bytes =
|
|
244
|
+
file[0] === 0x84 || file[0] === 0xd2
|
|
245
|
+
? file
|
|
246
|
+
: decodeBase64Text(new TextDecoder().decode(file), `grant file ${raw}`);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
bytes = decodeBase64Text(raw, "grant");
|
|
250
|
+
}
|
|
251
|
+
return { b64: bytesToBase64(bytes), view: inspectGrant(bytes) };
|
|
252
|
+
}
|
|
253
|
+
export function decodeBase64Text(text, what) {
|
|
254
|
+
const trimmed = text.trim();
|
|
255
|
+
if (!/^[A-Za-z0-9+/_-]+={0,2}$/.test(trimmed))
|
|
256
|
+
throw new InputError(`${what} is neither an existing file nor base64 (standard or URL-safe)`);
|
|
257
|
+
const bytes = base64ToBytes(trimmed);
|
|
258
|
+
if (bytes.length === 0)
|
|
259
|
+
throw new InputError(`${what} decoded to 0 bytes`);
|
|
260
|
+
return bytes;
|
|
261
|
+
}
|
|
262
|
+
/* ----------------------------- get_log_key ------------------------------ */
|
|
263
|
+
export function makeGetLogKeyTool(deps) {
|
|
264
|
+
// `created` is reported once per key, by the first call after this server
|
|
265
|
+
// made it; later calls see an existing key.
|
|
266
|
+
const announced = new Set();
|
|
267
|
+
return (args = {}) => guardHandler("get_log_key", async () => {
|
|
268
|
+
const role = args.which ?? "log";
|
|
269
|
+
const signer = await signerFor(deps, role)();
|
|
270
|
+
const xy = await signer.publicKeyXY();
|
|
271
|
+
const created = signer.created === true && !announced.has(role);
|
|
272
|
+
announced.add(role);
|
|
273
|
+
const structured = {
|
|
274
|
+
which: role,
|
|
275
|
+
alg: "ES256",
|
|
276
|
+
publicKeyXYHex: toHex(xy),
|
|
277
|
+
publicKeyXYBase64: bytesToBase64(xy),
|
|
278
|
+
publicKeyPem: publicKeyPem(xy),
|
|
279
|
+
...(role === "log" ? { statementKidHex: toHex(xy.slice(0, 32)) } : {}),
|
|
280
|
+
backend: signer.description,
|
|
281
|
+
created,
|
|
282
|
+
};
|
|
283
|
+
const use = role === "log"
|
|
284
|
+
? "A grant must name this key in its grantData."
|
|
285
|
+
: "The owner of the root above you names this key when it creates your authority log (forestrie-cli create-log --auth-log --child-payment-required --signer-pem <publicKeyPem saved to a file>); issue_grant then signs child grants with it.";
|
|
286
|
+
return ok(`${role} key (ES256) x‖y ${structured.publicKeyXYHex}${created ? " — newly created by this server" : ""}; held in ${signer.description}. ${use}`, structured);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
/* -------------------------- register_statement -------------------------- */
|
|
290
|
+
export function makeRegisterStatementTool(deps) {
|
|
291
|
+
return (args) => guardHandler("register_statement", async () => {
|
|
292
|
+
const baseUrl = args.baseUrl ?? deps.env["FORESTRIE_BASE_URL"];
|
|
293
|
+
if (!baseUrl)
|
|
294
|
+
return missingInput("register_statement", "baseUrl", "FORESTRIE_BASE_URL");
|
|
295
|
+
const rootLogId = args.rootLogId ?? deps.env["FORESTRIE_ROOT_LOG_ID"];
|
|
296
|
+
if (!rootLogId)
|
|
297
|
+
return missingInput("register_statement", "rootLogId", "FORESTRIE_ROOT_LOG_ID");
|
|
298
|
+
// Inputs first: a refused path must fail before anything else runs.
|
|
299
|
+
const payload = readBytes(args.payload, "payload", deps);
|
|
300
|
+
const grant = resolveGrant(args.grant, deps);
|
|
301
|
+
if (!grant)
|
|
302
|
+
return missingInput("register_statement", "grant", "FORESTRIE_GRANT");
|
|
303
|
+
const signer = await deps.signer();
|
|
304
|
+
const xy = await signer.publicKeyXY();
|
|
305
|
+
const refusals = statementGrantProblems(grant.view, xy);
|
|
306
|
+
if (refusals.length > 0)
|
|
307
|
+
return problemResult("register_statement", {
|
|
308
|
+
code: "grant_unusable",
|
|
309
|
+
message: refusals.join("; "),
|
|
310
|
+
logId: grant.view.logId,
|
|
311
|
+
});
|
|
312
|
+
if (payload.length > MAX_STATEMENT_BYTES)
|
|
313
|
+
return problemResult("register_statement", {
|
|
314
|
+
code: "too_large",
|
|
315
|
+
message: `payload is ${payload.length} bytes; canopy accepts signed statements up to ${MAX_STATEMENT_BYTES} bytes. Register a small manifest naming the artefact's digest instead`,
|
|
316
|
+
});
|
|
317
|
+
const signed = await signStatement({
|
|
318
|
+
payload,
|
|
319
|
+
contentType: args.contentType,
|
|
320
|
+
...(args.subject !== undefined ? { subject: args.subject } : {}),
|
|
321
|
+
...(args.issuer !== undefined ? { issuer: args.issuer } : {}),
|
|
322
|
+
issuedAt: Math.floor(deps.now().getTime() / 1000),
|
|
323
|
+
}, signer);
|
|
324
|
+
// Save first: once the request leaves, canopy may hold the statement
|
|
325
|
+
// even if we never see its answer, and a retry would sign a different
|
|
326
|
+
// one. The content hash names it either way.
|
|
327
|
+
const record = {
|
|
328
|
+
contentHash: signed.contentHash,
|
|
329
|
+
state: "signed",
|
|
330
|
+
baseUrl,
|
|
331
|
+
rootLogId,
|
|
332
|
+
logId: grant.view.logId,
|
|
333
|
+
contentType: args.contentType,
|
|
334
|
+
issuer: signed.issuer,
|
|
335
|
+
subject: signed.subject,
|
|
336
|
+
signedAt: deps.now().toISOString(),
|
|
337
|
+
};
|
|
338
|
+
deps.store.saveStatement(record, signed.statement);
|
|
339
|
+
const saved = {
|
|
340
|
+
contentHash: record.contentHash,
|
|
341
|
+
statementPath: deps.store.statementPath(record.contentHash),
|
|
342
|
+
};
|
|
343
|
+
let res;
|
|
344
|
+
try {
|
|
345
|
+
res = await postStatement({
|
|
346
|
+
baseUrl,
|
|
347
|
+
rootLogId,
|
|
348
|
+
statement: signed.statement,
|
|
349
|
+
grantB64: grant.b64,
|
|
350
|
+
}, { fetchImpl: deps.fetchImpl });
|
|
351
|
+
}
|
|
352
|
+
catch (err) {
|
|
353
|
+
if (!(err instanceof NetError))
|
|
354
|
+
throw err;
|
|
355
|
+
return problemResult("register_statement", {
|
|
356
|
+
code: err.code,
|
|
357
|
+
message: `${err.message}. The request may or may not have reached canopy; the signed statement is saved, so call check_registration with ${record.contentHash} before registering again`,
|
|
358
|
+
...saved,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
const outcome = readRegisterResponse(res, originOf(baseUrl));
|
|
362
|
+
if (outcome.kind === "problem")
|
|
363
|
+
return problemResult("register_statement", {
|
|
364
|
+
...httpProblemValue("canopy refused the statement", outcome.problem),
|
|
365
|
+
...saved,
|
|
366
|
+
});
|
|
367
|
+
record.state = "accepted";
|
|
368
|
+
record.statusUrl = outcome.statusUrl;
|
|
369
|
+
record.acceptedAt = res.at;
|
|
370
|
+
deps.store.saveRecord(record);
|
|
371
|
+
return ok(`registered: accepted by canopy for log ${record.logId}, content hash ${record.contentHash}. Not yet sequenced: call check_registration with that content hash${outcome.retryAfterSeconds !== undefined ? ` in about ${outcome.retryAfterSeconds}s` : ""}.`, {
|
|
372
|
+
...record,
|
|
373
|
+
...saved,
|
|
374
|
+
statementBytes: signed.statement.length,
|
|
375
|
+
...(outcome.retryAfterSeconds !== undefined
|
|
376
|
+
? { retryAfterSeconds: outcome.retryAfterSeconds }
|
|
377
|
+
: {}),
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
/* -------------------------- check_registration -------------------------- */
|
|
382
|
+
export function makeCheckRegistrationTool(deps) {
|
|
383
|
+
return (args) => guardHandler("check_registration", async () => {
|
|
384
|
+
const record = deps.store.load(args.contentHash);
|
|
385
|
+
const paths = {
|
|
386
|
+
statementPath: deps.store.statementPath(record.contentHash),
|
|
387
|
+
receiptPath: deps.store.receiptPath(record.contentHash),
|
|
388
|
+
};
|
|
389
|
+
if (record.sealedAt !== undefined &&
|
|
390
|
+
deps.store.readReceipt(record.contentHash))
|
|
391
|
+
return ok(`sealed: entry ${record.entryId}; receipt at ${paths.receiptPath}`, { ...record, state: "sealed", ...paths });
|
|
392
|
+
const origin = originOf(record.baseUrl);
|
|
393
|
+
let receiptUrl = record.receiptUrl;
|
|
394
|
+
if (receiptUrl === undefined) {
|
|
395
|
+
// canopy's status URL is a function of the coordinates, so a
|
|
396
|
+
// statement saved but never confirmed (state "signed") can still be
|
|
397
|
+
// looked up.
|
|
398
|
+
const statusUrl = record.statusUrl ??
|
|
399
|
+
registrationStatusUrl(record.baseUrl, record.rootLogId, record.logId, record.contentHash);
|
|
400
|
+
const res = await getRegistrationStatus({ statusUrl }, { fetchImpl: deps.fetchImpl });
|
|
401
|
+
const status = readStatusResponse(res, origin);
|
|
402
|
+
if (status.kind === "problem")
|
|
403
|
+
return httpProblem("check_registration", "the status URL answered with a problem", status.problem);
|
|
404
|
+
if (status.kind === "pending") {
|
|
405
|
+
record.firstPendingAt ??= res.at;
|
|
406
|
+
deps.store.saveRecord(record);
|
|
407
|
+
const minutes = (Date.parse(res.at) - Date.parse(record.firstPendingAt)) / 60_000;
|
|
408
|
+
const stale = minutes >= STALE_PENDING_MINUTES;
|
|
409
|
+
return ok(stale
|
|
410
|
+
? `pending for ${Math.floor(minutes)} minutes. canopy answers a statement it never received exactly as it answers one not yet sequenced, so this may never resolve${record.state === "signed" ? " (canopy never confirmed accepting it)" : ""}; consider registering again`
|
|
411
|
+
: `pending: not yet sequenced; check again in ${status.retryAfterSeconds}s`, {
|
|
412
|
+
state: "pending",
|
|
413
|
+
contentHash: record.contentHash,
|
|
414
|
+
retryAfterSeconds: status.retryAfterSeconds,
|
|
415
|
+
firstPendingAt: record.firstPendingAt,
|
|
416
|
+
...(stale ? { stale: true } : {}),
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
record.receiptUrl = status.receiptUrl;
|
|
420
|
+
record.entryId = status.entryId;
|
|
421
|
+
record.massifHeight = status.massifHeight;
|
|
422
|
+
deps.store.saveRecord(record);
|
|
423
|
+
receiptUrl = status.receiptUrl;
|
|
424
|
+
}
|
|
425
|
+
if (origin !== undefined && new URL(receiptUrl).origin !== origin)
|
|
426
|
+
return problemResult("check_registration", {
|
|
427
|
+
code: "invalid_location",
|
|
428
|
+
message: `the saved receipt URL ${receiptUrl} is not on ${origin}; not following it`,
|
|
429
|
+
});
|
|
430
|
+
const res = await fetchReceipt({ receiptUrl }, { fetchImpl: deps.fetchImpl });
|
|
431
|
+
const receipt = readReceiptResponse(res);
|
|
432
|
+
if (receipt.kind === "problem")
|
|
433
|
+
return httpProblem("check_registration", "the receipt URL answered with a problem", receipt.problem);
|
|
434
|
+
if (receipt.kind === "awaiting_seal")
|
|
435
|
+
return ok(`awaiting_seal: sequenced as entry ${record.entryId}; canopy says "${receipt.reason}". Sealing takes minutes, and needs delegate_sealing to have run for log ${record.logId}. Check again in about a minute.`, {
|
|
436
|
+
state: "awaiting_seal",
|
|
437
|
+
reason: receipt.reason,
|
|
438
|
+
contentHash: record.contentHash,
|
|
439
|
+
entryId: record.entryId,
|
|
440
|
+
massifHeight: record.massifHeight,
|
|
441
|
+
receiptUrl,
|
|
442
|
+
});
|
|
443
|
+
deps.store.saveReceipt(record.contentHash, receipt.receipt);
|
|
444
|
+
record.sealedAt = res.at;
|
|
445
|
+
deps.store.saveRecord(record);
|
|
446
|
+
return ok(`sealed: entry ${record.entryId}; receipt saved at ${paths.receiptPath}. Call verify_registration next.`, {
|
|
447
|
+
...record,
|
|
448
|
+
state: "sealed",
|
|
449
|
+
...paths,
|
|
450
|
+
receiptBytes: receipt.receipt.length,
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
/* -------------------------- verify_registration ------------------------- */
|
|
455
|
+
export function makeVerifyRegistrationTool(deps) {
|
|
456
|
+
return (args) => guardHandler("verify_registration", async () => {
|
|
457
|
+
const record = deps.store.load(args.contentHash);
|
|
458
|
+
const receipt = deps.store.readReceipt(record.contentHash);
|
|
459
|
+
if (receipt === undefined || record.entryId === undefined)
|
|
460
|
+
return problemResult("verify_registration", {
|
|
461
|
+
code: "not_sealed",
|
|
462
|
+
message: `registration ${record.contentHash} has no receipt yet: call check_registration until it reports sealed`,
|
|
463
|
+
});
|
|
464
|
+
const statement = deps.store.readStatement(record.contentHash);
|
|
465
|
+
let trust;
|
|
466
|
+
let rootFrom;
|
|
467
|
+
if (args.knownAccumulator !== undefined) {
|
|
468
|
+
trust = {
|
|
469
|
+
root: "known-accumulator",
|
|
470
|
+
accumulator: readBytes(args.knownAccumulator, "knownAccumulator", deps),
|
|
471
|
+
};
|
|
472
|
+
rootFrom = "supplied by caller";
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
// Never creates a key: verifying is read-only.
|
|
476
|
+
const signer = await deps.signer({ create: false });
|
|
477
|
+
trust = { root: "known-log-key", keyXy: await signer.publicKeyXY() };
|
|
478
|
+
rootFrom = `this server's log key (${signer.description})`;
|
|
479
|
+
}
|
|
480
|
+
const result = await verifyFetched({
|
|
481
|
+
kind: "payload",
|
|
482
|
+
receipt,
|
|
483
|
+
payload: statement,
|
|
484
|
+
entryId: record.entryId,
|
|
485
|
+
trust,
|
|
486
|
+
rootProvenance: "supplied",
|
|
487
|
+
});
|
|
488
|
+
const provenance = `statement and receipt from ${deps.store.dir(record.contentHash)}; receipt fetched from ${record.receiptUrl} at ${record.sealedAt}; root ${trust.root} from ${rootFrom}`;
|
|
489
|
+
return ok(summarizeFetched("verify_registration", result, provenance), {
|
|
490
|
+
contentHash: record.contentHash,
|
|
491
|
+
entryId: record.entryId,
|
|
492
|
+
root: trust.root,
|
|
493
|
+
rootFrom,
|
|
494
|
+
result: result,
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
/* --------------------------- delegate_sealing --------------------------- */
|
|
499
|
+
export function makeDelegateSealingTool(deps) {
|
|
500
|
+
return (args) => guardHandler("delegate_sealing", async () => {
|
|
501
|
+
const baseUrl = args.baseUrl ?? deps.env["FORESTRIE_BASE_URL"];
|
|
502
|
+
if (!baseUrl)
|
|
503
|
+
return missingInput("delegate_sealing", "baseUrl", "FORESTRIE_BASE_URL");
|
|
504
|
+
const coordinatorUrl = args.coordinatorUrl ?? deps.env["FORESTRIE_COORDINATOR_URL"];
|
|
505
|
+
if (!coordinatorUrl)
|
|
506
|
+
return missingInput("delegate_sealing", "coordinatorUrl", "FORESTRIE_COORDINATOR_URL");
|
|
507
|
+
const sealerKeyB64 = args.knownSealerKey ?? deps.env["FORESTRIE_KNOWN_SEALER_KEY"];
|
|
508
|
+
if (!sealerKeyB64)
|
|
509
|
+
return missingInput("delegate_sealing", "knownSealerKey", "FORESTRIE_KNOWN_SEALER_KEY");
|
|
510
|
+
const knownSealerKeyXY = decodeBase64Text(sealerKeyB64, "knownSealerKey");
|
|
511
|
+
if (knownSealerKeyXY.length !== 64)
|
|
512
|
+
throw new InputError(`knownSealerKey must decode to 64 bytes x‖y (got ${knownSealerKeyXY.length})`);
|
|
513
|
+
const role = args.as ?? "log";
|
|
514
|
+
const grantEnv = role === "authority" ? "FORESTRIE_AUTHORITY_GRANT" : "FORESTRIE_GRANT";
|
|
515
|
+
const grant = resolveGrant(args.grant, deps, grantEnv);
|
|
516
|
+
if (!grant)
|
|
517
|
+
return missingInput("delegate_sealing", "grant", grantEnv);
|
|
518
|
+
const signer = await signerFor(deps, role)();
|
|
519
|
+
const xy = await signer.publicKeyXY();
|
|
520
|
+
if (grant.view.grantData.length !== 64 ||
|
|
521
|
+
toHex(grant.view.grantData) !== toHex(xy))
|
|
522
|
+
return problemResult("delegate_sealing", {
|
|
523
|
+
code: "grant_unusable",
|
|
524
|
+
message: `the grant's grantData does not name this server's ${role} key, so this key does not own log ${grant.view.logId} and cannot delegate its sealing`,
|
|
525
|
+
});
|
|
526
|
+
const outcome = await delegateSealing({
|
|
527
|
+
baseUrl,
|
|
528
|
+
coordinatorUrl,
|
|
529
|
+
knownSealerKeyXY,
|
|
530
|
+
grantB64: grant.b64,
|
|
531
|
+
grant: grant.view,
|
|
532
|
+
...(args.ttlSeconds !== undefined
|
|
533
|
+
? { ttlSeconds: args.ttlSeconds }
|
|
534
|
+
: {}),
|
|
535
|
+
...(deps.sleep !== undefined ? { sleep: deps.sleep } : {}),
|
|
536
|
+
}, signer, { fetchImpl: deps.fetchImpl });
|
|
537
|
+
if (outcome.kind === "problem")
|
|
538
|
+
return problemResult("delegate_sealing", {
|
|
539
|
+
code: `${outcome.stage}_failed`,
|
|
540
|
+
stage: outcome.stage,
|
|
541
|
+
message: outcome.message,
|
|
542
|
+
prepared: outcome.prepared,
|
|
543
|
+
...(outcome.problem !== undefined ? { http: outcome.problem } : {}),
|
|
544
|
+
});
|
|
545
|
+
return ok(`delegated: sealer ${outcome.sealerId} (epoch ${outcome.epoch}) may seal log ${outcome.logId} until ${new Date(outcome.expiresAt * 1000).toISOString()}${outcome.prepared ? "; ran prepare first to publish the log's public root" : ""}.`, { state: "delegated", ...outcome });
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
//# sourceMappingURL=tools.js.map
|