ankka 0.11.0 → 0.13.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 +1 -0
- package/dist/_proto/ankka/protocol/v1/client_pb.d.ts +910 -0
- package/dist/_proto/ankka/protocol/v1/client_pb.js +239 -23
- package/dist/_proto/ankka/protocol/v1/consumer_pb.d.ts +10 -1
- package/dist/_proto/ankka/protocol/v1/consumer_pb.js +1 -1
- package/dist/_proto/ankka/protocol/v1/discovery_pb.d.ts +177 -0
- package/dist/_proto/ankka/protocol/v1/discovery_pb.js +51 -36
- package/dist/_proto/ankka/protocol/v1/endpoint_pb.d.ts +26 -0
- package/dist/_proto/ankka/protocol/v1/endpoint_pb.js +15 -10
- package/dist/_proto/ankka/protocol/v1/event_sourced_pb.d.ts +86 -0
- package/dist/_proto/ankka/protocol/v1/event_sourced_pb.js +11 -1
- package/dist/_proto/ankka/protocol/v1/mcp_pb.d.ts +120 -0
- package/dist/_proto/ankka/protocol/v1/mcp_pb.js +29 -0
- package/dist/_proto/ankka/protocol/v1/payload_pb.d.ts +61 -0
- package/dist/_proto/ankka/protocol/v1/payload_pb.js +18 -1
- package/dist/_proto/ankka/protocol/v1/view_pb.d.ts +7 -1
- package/dist/_proto/ankka/protocol/v1/view_pb.js +1 -1
- package/dist/_proto/ankka/protocol/v1/wasm_pb.d.ts +12 -0
- package/dist/_proto/ankka/protocol/v1/wasm_pb.js +1 -1
- package/dist/agent.d.ts +10 -0
- package/dist/agent.js +20 -0
- package/dist/autonomous.d.ts +5 -0
- package/dist/autonomous.js +10 -0
- package/dist/client-keys.d.ts +89 -0
- package/dist/client-keys.js +182 -0
- package/dist/client.d.ts +96 -3
- package/dist/client.js +228 -6
- package/dist/consumer.d.ts +23 -1
- package/dist/consumer.js +25 -1
- package/dist/context.d.ts +25 -1
- package/dist/context.js +5 -2
- package/dist/contract.d.ts +4 -0
- package/dist/contract.js +2 -0
- package/dist/effects/common.d.ts +7 -0
- package/dist/effects/common.js +9 -1
- package/dist/effects/eventSourced.d.ts +26 -1
- package/dist/effects/eventSourced.js +58 -5
- package/dist/endpoint.d.ts +10 -0
- package/dist/endpoint.js +20 -0
- package/dist/eventSourcedEntity.d.ts +8 -3
- package/dist/graph.d.ts +14 -1
- package/dist/graph.js +17 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.js +8 -2
- package/dist/keyedView.d.ts +4 -1
- package/dist/keyedView.js +7 -1
- package/dist/kinds.js +2 -0
- package/dist/mcp-endpoint.d.ts +144 -0
- package/dist/mcp-endpoint.js +256 -0
- package/dist/routes.d.ts +46 -3
- package/dist/routes.js +56 -3
- package/dist/server/discovery.d.ts +12 -0
- package/dist/server/discovery.js +115 -4
- package/dist/server/eventSourced.d.ts +4 -1
- package/dist/server/eventSourced.js +24 -6
- package/dist/server/http.d.ts +8 -0
- package/dist/server/http.js +12 -4
- package/dist/server/mcp.d.ts +17 -0
- package/dist/server/mcp.js +157 -0
- package/dist/server/server.d.ts +11 -1
- package/dist/server/server.js +19 -0
- package/dist/server/stateless.js +5 -4
- package/dist/server/storageEndpoint.d.ts +14 -0
- package/dist/server/storageEndpoint.js +82 -0
- package/dist/service.d.ts +12 -1
- package/dist/service.js +73 -5
- package/dist/sigv4.d.ts +47 -0
- package/dist/sigv4.js +149 -0
- package/dist/spec.d.ts +8 -2
- package/dist/spec.js +62 -6
- package/dist/standing.d.ts +18 -0
- package/dist/standing.js +26 -0
- package/dist/startFrom.d.ts +4 -0
- package/dist/startFrom.js +8 -1
- package/dist/storage.d.ts +288 -0
- package/dist/storage.js +1081 -0
- package/dist/testkit/index.d.ts +1 -1
- package/dist/testkit/integration.d.ts +9 -0
- package/dist/testkit/integration.js +5 -1
- package/dist/testkit/kinds.d.ts +25 -9
- package/dist/testkit/kinds.js +53 -28
- package/dist/testkit/unit.d.ts +31 -3
- package/dist/testkit/unit.js +55 -11
- package/dist/timedAction.d.ts +10 -0
- package/dist/timedAction.js +20 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/view.d.ts +18 -3
- package/dist/view.js +13 -4
- package/dist/workflow.d.ts +8 -0
- package/dist/workflow.js +15 -0
- package/package.json +1 -1
package/dist/server/server.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Registry } from "../service.ts";
|
|
2
2
|
import type { ComponentClient } from "../client.ts";
|
|
3
|
+
import { type Buckets } from "../storage.ts";
|
|
3
4
|
export interface ServerOptions {
|
|
4
5
|
/** `127.0.0.1` by default; `0.0.0.0` for the testkit; anything else is refused. */
|
|
5
6
|
readonly host?: string;
|
|
@@ -8,12 +9,21 @@ export interface ServerOptions {
|
|
|
8
9
|
/** The largest message accepted from the sidecar; 64 MiB by default. */
|
|
9
10
|
readonly readMaxBytes?: number;
|
|
10
11
|
readonly log?: (message: string) => void;
|
|
12
|
+
/**
|
|
13
|
+
* The service's buckets, in place of the store `ANKKA_STORAGE_BACKEND` and the `ANKKA_S3_*` variables
|
|
14
|
+
* choose: the integration testkit's in-memory store.
|
|
15
|
+
*/
|
|
16
|
+
readonly storage?: Buckets;
|
|
17
|
+
/** Where `service.json` is read from for the buckets it declares; the working directory by default. */
|
|
18
|
+
readonly serviceDirectory?: string;
|
|
11
19
|
}
|
|
12
20
|
/** What every servicer needs. */
|
|
13
21
|
export interface ServerContext {
|
|
14
22
|
readonly registry: Registry;
|
|
15
23
|
readonly client: ComponentClient;
|
|
16
24
|
readonly log: (message: string) => void;
|
|
25
|
+
/** The buckets every endpoint instance is given; the process's store when absent. */
|
|
26
|
+
readonly storage?: Buckets;
|
|
17
27
|
}
|
|
18
28
|
export declare class Server {
|
|
19
29
|
#private;
|
package/dist/server/server.js
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
// the integration testkit, whose sidecar reaches this process from a container.
|
|
5
5
|
import { createServer } from "node:http2";
|
|
6
6
|
import { connectNodeAdapter } from "@connectrpc/connect-node";
|
|
7
|
+
import { Registry } from "../service.js";
|
|
8
|
+
import { _setProcessStorage, chooseStorage, DirectoryBuckets } from "../storage.js";
|
|
9
|
+
import { DirectoryStorageEndpoint } from "./storageEndpoint.js";
|
|
7
10
|
import { renderSpec } from "../spec.js";
|
|
8
11
|
import { discoveryRoutes } from "./discovery.js";
|
|
9
12
|
import { eventSourcedRoutes } from "./eventSourced.js";
|
|
@@ -12,6 +15,13 @@ import { workflowRoutes } from "./workflow.js";
|
|
|
12
15
|
import { statelessRoutes } from "./stateless.js";
|
|
13
16
|
import { agentRoutes } from "./agent.js";
|
|
14
17
|
import { httpRoutes } from "./http.js";
|
|
18
|
+
import { mcpRoutes } from "./mcp.js";
|
|
19
|
+
/** The registry with the directory store's endpoint, which serves the URLs that store signs. */
|
|
20
|
+
function withStorageEndpoint(registry) {
|
|
21
|
+
const cls = DirectoryStorageEndpoint;
|
|
22
|
+
const endpoint = Object.freeze({ id: cls.name, cls, prefix: cls.prefix, acl: cls.acl, routes: new Map(Object.entries(cls.routes)) });
|
|
23
|
+
return new Registry(registry.components, new Map([...registry.endpoints, [endpoint.id, endpoint]]));
|
|
24
|
+
}
|
|
15
25
|
const LOOPBACK = new Set(["127.0.0.1", "localhost", "::1", "0.0.0.0", "::"]);
|
|
16
26
|
export class Server {
|
|
17
27
|
#context;
|
|
@@ -39,6 +49,14 @@ export class Server {
|
|
|
39
49
|
const port = this.#options.port ?? Number(process.env.ANKKA_PROCESS_PORT ?? 9010);
|
|
40
50
|
if (!Number.isInteger(port) || port < 0 || port > 65535)
|
|
41
51
|
throw new Error(`not a port: ${String(port)}`);
|
|
52
|
+
// The service's buckets: the store given, or the one the environment chooses. A directory store's
|
|
53
|
+
// URLs are answered by an endpoint this process declares at /_ankka/storage.
|
|
54
|
+
const given = this.#options.storage;
|
|
55
|
+
const chosen = given ? { backend: "given", buckets: given } : chooseStorage(process.env, { ...(this.#options.serviceDirectory ? { serviceDirectory: this.#options.serviceDirectory } : {}) });
|
|
56
|
+
_setProcessStorage(chosen.buckets);
|
|
57
|
+
const registry = chosen.buckets instanceof DirectoryBuckets ? withStorageEndpoint(this.#context.registry) : this.#context.registry;
|
|
58
|
+
this.#context = { ...this.#context, registry, storage: chosen.buckets };
|
|
59
|
+
this.#context.log(`ankka: object storage is ${chosen.backend}${chosen.buckets instanceof DirectoryBuckets ? ` at ${chosen.buckets.root}` : ""}`);
|
|
42
60
|
const handler = connectNodeAdapter({
|
|
43
61
|
routes: (router) => {
|
|
44
62
|
discoveryRoutes(router, () => renderSpec(this.#context.registry), this.#context.log);
|
|
@@ -48,6 +66,7 @@ export class Server {
|
|
|
48
66
|
statelessRoutes(router, this.#context);
|
|
49
67
|
agentRoutes(router, this.#context);
|
|
50
68
|
httpRoutes(router, this.#context);
|
|
69
|
+
mcpRoutes(router, this.#context);
|
|
51
70
|
},
|
|
52
71
|
shutdownSignal: this.#shutdown.signal,
|
|
53
72
|
readMaxBytes: this.#options.readMaxBytes ?? 64 * 1024 * 1024,
|
package/dist/server/stateless.js
CHANGED
|
@@ -15,6 +15,7 @@ import { JSON_CONTENT } from "../codec.js";
|
|
|
15
15
|
import { errorCodeToProto } from "../kinds.js";
|
|
16
16
|
import { decodePayload, encodePayload } from "./payloads.js";
|
|
17
17
|
import { PayloadSchema } from "../_proto/ankka/protocol/v1/payload_pb.js";
|
|
18
|
+
import { standingFromProto } from "../standing.js";
|
|
18
19
|
function messageOf(e) {
|
|
19
20
|
return e instanceof Error ? `${e.name}: ${e.message}` : String(e);
|
|
20
21
|
}
|
|
@@ -33,7 +34,7 @@ async function handleKeyedView(req, sourceId, ctx) {
|
|
|
33
34
|
const view = new registered.cls();
|
|
34
35
|
try {
|
|
35
36
|
const client = ctx.client.withMetadata(metadata);
|
|
36
|
-
view._bind(metadata, client, clientRows(client, registered.id, registered.rowCodec));
|
|
37
|
+
view._bind(metadata, client, clientRows(client, registered.id, registered.rowCodec), standingFromProto(req.standing));
|
|
37
38
|
const effect = (req.deleted
|
|
38
39
|
? source.deleted
|
|
39
40
|
? await source.deleted(view)
|
|
@@ -63,7 +64,7 @@ export async function handleView(req, ctx) {
|
|
|
63
64
|
const view = new registered.cls();
|
|
64
65
|
try {
|
|
65
66
|
const row = req.row ? decodePayload(registered.rowCodec, req.row) : null;
|
|
66
|
-
view._bind(row, metadata, ctx.client.withMetadata(metadata));
|
|
67
|
+
view._bind(row, metadata, ctx.client.withMetadata(metadata), standingFromProto(req.standing));
|
|
67
68
|
const effect = req.deleted ? await view.onDelete() : await view.onChange(decodePayload(registered.eventCodec, req.event));
|
|
68
69
|
switch (effect.kind) {
|
|
69
70
|
case "update-row":
|
|
@@ -102,7 +103,7 @@ function produceAll(request, messages) {
|
|
|
102
103
|
}
|
|
103
104
|
async function graphEffect(registered, req, metadata, ctx) {
|
|
104
105
|
const consumer = new registered.cls();
|
|
105
|
-
consumer._bind(metadata, ctx.client.withMetadata(metadata));
|
|
106
|
+
consumer._bind(metadata, ctx.client.withMetadata(metadata), standingFromProto(req.standing));
|
|
106
107
|
const effect = req.deleted ? await consumer.onDelete() : await consumer.onMessage(decodePayload(registered.messageCodec, req.message));
|
|
107
108
|
switch (effect.kind) {
|
|
108
109
|
case "publish":
|
|
@@ -121,7 +122,7 @@ async function graphEffect(registered, req, metadata, ctx) {
|
|
|
121
122
|
}
|
|
122
123
|
async function consumerEffect(registered, req, metadata, ctx) {
|
|
123
124
|
const consumer = new registered.cls();
|
|
124
|
-
consumer._bind(metadata, ctx.client.withMetadata(metadata));
|
|
125
|
+
consumer._bind(metadata, ctx.client.withMetadata(metadata), standingFromProto(req.standing));
|
|
125
126
|
const effect = req.deleted ? await consumer.onDelete() : await consumer.onMessage(decodePayload(registered.messageCodec, req.message));
|
|
126
127
|
const outCodec = () => {
|
|
127
128
|
if (!registered.outCodec)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Endpoint } from "../endpoint.ts";
|
|
2
|
+
import { Acl, RawReply, type RouteRef } from "../routes.ts";
|
|
3
|
+
type Params = Readonly<Record<string, string>>;
|
|
4
|
+
export declare class DirectoryStorageEndpoint extends Endpoint {
|
|
5
|
+
#private;
|
|
6
|
+
static readonly prefix = "/_ankka/storage";
|
|
7
|
+
static readonly acl: Acl;
|
|
8
|
+
/** @internal The SDK's own endpoint under `/_ankka`, which registration admits. */
|
|
9
|
+
static readonly _sdkReserved = true;
|
|
10
|
+
static readonly routes: Readonly<Record<string, RouteRef<DirectoryStorageEndpoint, any, any, any>>>;
|
|
11
|
+
read(params: Params, depth: number): Promise<RawReply>;
|
|
12
|
+
keep(params: Params, depth: number, body: Uint8Array): Promise<RawReply>;
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// What a TypeScript service on a developer's machine serves at `/_ankka/storage/<bucket>/<name>` while its
|
|
2
|
+
// store is a directory (feature 055, research R12): the signed URLs and public addresses that store
|
|
3
|
+
// makes, answered through the sidecar like any route, so a browser reads and keeps through them as it
|
|
4
|
+
// would through an object store's. The runtime's `DirectoryStorageEndpoint` says the same in Scala.
|
|
5
|
+
//
|
|
6
|
+
// The token is the whole of the authority, as a signature is on an object store: a `GET` reads with a
|
|
7
|
+
// token for `read`, or with none on a public bucket; a `PUT` keeps with a token for `keep`. Anything
|
|
8
|
+
// else is `403`, and an object not there `404`. The sidecar matches a route by whole segments and an
|
|
9
|
+
// object's name holds slashes, so one route is declared per depth, up to `MAX_ADDRESS_SEGMENTS`; a
|
|
10
|
+
// deeper name travels as one segment with its slashes encoded.
|
|
11
|
+
import { binaryCodecs } from "../codec.js";
|
|
12
|
+
import { Endpoint } from "../endpoint.js";
|
|
13
|
+
import { Acl, get, HttpProblem, put, RawReply } from "../routes.js";
|
|
14
|
+
import { DirectoryBuckets, MAX_ADDRESS_SEGMENTS, STORAGE_PATH } from "../storage.js";
|
|
15
|
+
/** A segment as the sidecar forwards it, encoded; one that is not valid encoding is taken as it is. */
|
|
16
|
+
function decoded(segment) {
|
|
17
|
+
try {
|
|
18
|
+
return decodeURIComponent(segment);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return segment;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** The bucket's segment and the object's name a request's path parameters spell. */
|
|
25
|
+
function addressed(params, depth) {
|
|
26
|
+
const parts = [];
|
|
27
|
+
for (let i = 1; i <= depth; i++)
|
|
28
|
+
parts.push(decoded(params[`n${i}`] ?? ""));
|
|
29
|
+
return { segment: decoded(params.bucket ?? ""), objectName: parts.join("/") };
|
|
30
|
+
}
|
|
31
|
+
const NOWHERE = "no object at this address";
|
|
32
|
+
const UNSIGNED = "this address is not signed for that, or its signature has expired";
|
|
33
|
+
export class DirectoryStorageEndpoint extends Endpoint {
|
|
34
|
+
static prefix = STORAGE_PATH;
|
|
35
|
+
static acl = Acl.allowAll;
|
|
36
|
+
/** @internal The SDK's own endpoint under `/_ankka`, which registration admits. */
|
|
37
|
+
static _sdkReserved = true;
|
|
38
|
+
static routes = (() => {
|
|
39
|
+
const table = {};
|
|
40
|
+
for (let depth = 1; depth <= MAX_ADDRESS_SEGMENTS; depth++) {
|
|
41
|
+
const template = "/{bucket}" + Array.from({ length: depth }, (_, i) => `/{n${i + 1}}`).join("");
|
|
42
|
+
table[`read${depth}`] = get(template, binaryCodecs.bytes, (ep, req) => ep.read(req.params, depth));
|
|
43
|
+
table[`keep${depth}`] = put(template, binaryCodecs.bytes, binaryCodecs.bytes, (ep, req, body) => ep.keep(req.params, depth, body));
|
|
44
|
+
}
|
|
45
|
+
return Object.freeze(table);
|
|
46
|
+
})();
|
|
47
|
+
#store(segment) {
|
|
48
|
+
const store = this.storage;
|
|
49
|
+
if (!(store instanceof DirectoryBuckets))
|
|
50
|
+
throw new HttpProblem(404, NOWHERE);
|
|
51
|
+
const found = store.bySegment(segment);
|
|
52
|
+
if (!found)
|
|
53
|
+
throw new HttpProblem(404, NOWHERE);
|
|
54
|
+
return { store, bucket: found.bucket, segment: found.segment };
|
|
55
|
+
}
|
|
56
|
+
#signedFor(store, segment, objectName, access) {
|
|
57
|
+
const q = this.request.query;
|
|
58
|
+
const expires = Number(q.get("expires") ?? "");
|
|
59
|
+
const token = q.get("token");
|
|
60
|
+
return q.get("access") === access && token !== null && Number.isInteger(expires) && store.admits(segment, objectName, access, expires, token);
|
|
61
|
+
}
|
|
62
|
+
async read(params, depth) {
|
|
63
|
+
const { segment, objectName } = addressed(params, depth);
|
|
64
|
+
const { store, bucket } = this.#store(segment);
|
|
65
|
+
const open = bucket.exposure === "public" && bucket.name !== undefined;
|
|
66
|
+
if (!open && !this.#signedFor(store, segment, objectName, "read"))
|
|
67
|
+
throw new HttpProblem(403, UNSIGNED);
|
|
68
|
+
const object = await bucket.get(objectName);
|
|
69
|
+
if (!object)
|
|
70
|
+
throw new HttpProblem(404, `no object '${objectName}'`);
|
|
71
|
+
return new RawReply(200, object.contentType, await object.bytes());
|
|
72
|
+
}
|
|
73
|
+
async keep(params, depth, body) {
|
|
74
|
+
const { segment, objectName } = addressed(params, depth);
|
|
75
|
+
const { store, bucket } = this.#store(segment);
|
|
76
|
+
if (!this.#signedFor(store, segment, objectName, "keep"))
|
|
77
|
+
throw new HttpProblem(403, UNSIGNED);
|
|
78
|
+
const contentType = this.request.headers.get("content-type") ?? "application/octet-stream";
|
|
79
|
+
await bucket.put(objectName, body, { contentType });
|
|
80
|
+
return new RawReply(200, "text/plain; charset=utf-8", new Uint8Array());
|
|
81
|
+
}
|
|
82
|
+
}
|
package/dist/service.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { Agent, type AgentClass } from "./agent.ts";
|
|
|
13
13
|
import { type McpServerRef, type ResultGuardrailRef } from "./mcp.ts";
|
|
14
14
|
import { AutonomousAgent, type AutonomousAgentClass, type TaskType } from "./autonomous.ts";
|
|
15
15
|
import { Endpoint, type EndpointClass } from "./endpoint.ts";
|
|
16
|
+
import { McpEndpoint, type McpEndpointClass, type RegisteredMcpEndpoint } from "./mcp-endpoint.ts";
|
|
16
17
|
import type { GuardrailRef, HandlerRef, ToolRef } from "./handlers.ts";
|
|
17
18
|
import type { RouteRef, Acl } from "./routes.ts";
|
|
18
19
|
import { ComponentClient } from "./client.ts";
|
|
@@ -28,6 +29,8 @@ export interface RegisteredEventSourced {
|
|
|
28
29
|
readonly eventCodec: Codec<any>;
|
|
29
30
|
readonly handlers: ReadonlyMap<string, HandlerRef<any, any, any, any>>;
|
|
30
31
|
readonly snapshotEvery: number;
|
|
32
|
+
/** Declared replicated (1.19). */
|
|
33
|
+
readonly replicated: boolean;
|
|
31
34
|
}
|
|
32
35
|
export interface RegisteredKeyValue {
|
|
33
36
|
readonly kind: "key-value";
|
|
@@ -58,6 +61,8 @@ export type Source = {
|
|
|
58
61
|
readonly contract?: Contract;
|
|
59
62
|
/** The declared broker the topic is on; absent is the installation's. */
|
|
60
63
|
readonly broker?: string;
|
|
64
|
+
/** Another project's topic: the project whose topic it is (1.15). */
|
|
65
|
+
readonly project?: string;
|
|
61
66
|
/** Partitions handled at once, each in order. */
|
|
62
67
|
readonly parallel?: boolean;
|
|
63
68
|
};
|
|
@@ -102,6 +107,8 @@ export interface RegisteredConsumer {
|
|
|
102
107
|
/** A graph consumer: its handlers return elements, each published as a delta under its element key. */
|
|
103
108
|
readonly graph: boolean;
|
|
104
109
|
readonly version: number | undefined;
|
|
110
|
+
/** Given a replicated entity's mirrored changes too (1.19). */
|
|
111
|
+
readonly mirroredChanges: boolean;
|
|
105
112
|
}
|
|
106
113
|
export interface RegisteredTimedAction {
|
|
107
114
|
readonly kind: "timed-action";
|
|
@@ -144,7 +151,10 @@ export interface RegisteredEndpoint {
|
|
|
144
151
|
export declare class Registry {
|
|
145
152
|
readonly components: ReadonlyMap<string, RegisteredComponent>;
|
|
146
153
|
readonly endpoints: ReadonlyMap<string, RegisteredEndpoint>;
|
|
147
|
-
|
|
154
|
+
/** The MCP endpoints (1.20), by id, their references resolved. */
|
|
155
|
+
readonly mcpEndpoints: ReadonlyMap<string, RegisteredMcpEndpoint>;
|
|
156
|
+
constructor(components: ReadonlyMap<string, RegisteredComponent>, endpoints: ReadonlyMap<string, RegisteredEndpoint>, mcpEndpoints?: ReadonlyMap<string, RegisteredMcpEndpoint>);
|
|
157
|
+
mcpEndpoint(id: string): RegisteredMcpEndpoint | undefined;
|
|
148
158
|
component(id: string): RegisteredComponent | undefined;
|
|
149
159
|
/** The component `id` when it is of `kind`. */
|
|
150
160
|
of<K extends RegisteredComponent["kind"]>(kind: K, id: string): Extract<RegisteredComponent, {
|
|
@@ -179,6 +189,7 @@ export declare class ServiceBuilder {
|
|
|
179
189
|
register<C extends TimedAction>(cls: TimedActionClass<C>): this;
|
|
180
190
|
register<C extends Agent>(cls: AgentClass<C>): this;
|
|
181
191
|
register<C extends AutonomousAgent>(cls: AutonomousAgentClass<C>): this;
|
|
192
|
+
register<C extends McpEndpoint>(cls: McpEndpointClass<C>): this;
|
|
182
193
|
register<C extends Endpoint>(cls: EndpointClass<C>): this;
|
|
183
194
|
/** Validates every registration and returns the registry, or throws a `RegistrationError` naming every problem. */
|
|
184
195
|
validate(): Registry;
|
package/dist/service.js
CHANGED
|
@@ -18,7 +18,8 @@ import { Agent } from "./agent.js";
|
|
|
18
18
|
import { serverProblems, TOOL_PREFIX } from "./mcp.js";
|
|
19
19
|
import { AutonomousAgent, COMPLETE_TASK, FAIL_TASK } from "./autonomous.js";
|
|
20
20
|
import { Endpoint } from "./endpoint.js";
|
|
21
|
-
import {
|
|
21
|
+
import { McpEndpoint, registerMcpEndpoint } from "./mcp-endpoint.js";
|
|
22
|
+
import { isAcl, isEndpointAcl } from "./routes.js";
|
|
22
23
|
import { ComponentClient } from "./client.js";
|
|
23
24
|
import { Server } from "./server/server.js";
|
|
24
25
|
import { renderSpec } from "./spec.js";
|
|
@@ -26,11 +27,17 @@ import { renderSpec } from "./spec.js";
|
|
|
26
27
|
export class Registry {
|
|
27
28
|
components;
|
|
28
29
|
endpoints;
|
|
29
|
-
|
|
30
|
+
/** The MCP endpoints (1.20), by id, their references resolved. */
|
|
31
|
+
mcpEndpoints;
|
|
32
|
+
constructor(components, endpoints, mcpEndpoints = new Map()) {
|
|
30
33
|
this.components = components;
|
|
31
34
|
this.endpoints = endpoints;
|
|
35
|
+
this.mcpEndpoints = mcpEndpoints;
|
|
32
36
|
Object.freeze(this);
|
|
33
37
|
}
|
|
38
|
+
mcpEndpoint(id) {
|
|
39
|
+
return this.mcpEndpoints.get(id);
|
|
40
|
+
}
|
|
34
41
|
component(id) {
|
|
35
42
|
return this.components.get(id);
|
|
36
43
|
}
|
|
@@ -83,6 +90,7 @@ export class ServiceBuilder {
|
|
|
83
90
|
const components = new Map();
|
|
84
91
|
const endpoints = new Map();
|
|
85
92
|
const prefixes = new Map();
|
|
93
|
+
const mcpClasses = [];
|
|
86
94
|
const add = (r, cls) => {
|
|
87
95
|
if (!r)
|
|
88
96
|
return;
|
|
@@ -112,6 +120,9 @@ export class ServiceBuilder {
|
|
|
112
120
|
add(registerAgent(cls, problems), cls);
|
|
113
121
|
else if (extendsBase(cls, AutonomousAgent))
|
|
114
122
|
add(registerAutonomousAgent(cls, problems), cls);
|
|
123
|
+
// Before Endpoint: an MCP endpoint is one, served by the platform rather than by routes.
|
|
124
|
+
else if (extendsBase(cls, McpEndpoint))
|
|
125
|
+
mcpClasses.push(cls);
|
|
115
126
|
else if (extendsBase(cls, Endpoint)) {
|
|
116
127
|
const ep = cls;
|
|
117
128
|
if (typeof ep.prefix === "string") {
|
|
@@ -129,12 +140,35 @@ export class ServiceBuilder {
|
|
|
129
140
|
}
|
|
130
141
|
}
|
|
131
142
|
else {
|
|
132
|
-
problems.push(`${nameOf(cls)} is not a component class: it must extend EventSourcedEntity, KeyValueEntity, Workflow, View, KeyedView, Consumer, GraphConsumer, TimedAction, Agent, AutonomousAgent or
|
|
143
|
+
problems.push(`${nameOf(cls)} is not a component class: it must extend EventSourcedEntity, KeyValueEntity, Workflow, View, KeyedView, Consumer, GraphConsumer, TimedAction, Agent, AutonomousAgent, Endpoint or McpEndpoint`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// MCP endpoints last: their references name the components and endpoints registered above.
|
|
147
|
+
const mcpEndpoints = new Map();
|
|
148
|
+
const mcpPaths = new Map();
|
|
149
|
+
for (const cls of mcpClasses) {
|
|
150
|
+
const r = registerMcpEndpoint(cls, components, endpoints, problems);
|
|
151
|
+
if (!r)
|
|
152
|
+
continue;
|
|
153
|
+
const other = mcpPaths.get(r.path);
|
|
154
|
+
if (other)
|
|
155
|
+
problems.push(`MCP endpoints ${other} and ${r.id} share the path ${r.path}`);
|
|
156
|
+
mcpPaths.set(r.path, r.id);
|
|
157
|
+
for (const e of endpoints.values()) {
|
|
158
|
+
for (const route of e.routes.values()) {
|
|
159
|
+
const full = route.template === "/" ? e.prefix : e.prefix === "/" ? route.template : `${e.prefix}${route.template}`;
|
|
160
|
+
if (full === r.path)
|
|
161
|
+
problems.push(`${r.id}: its path ${r.path} is also the route ${route.method} ${full} of ${e.id}`);
|
|
162
|
+
}
|
|
133
163
|
}
|
|
164
|
+
if (mcpEndpoints.has(r.id))
|
|
165
|
+
problems.push(`two MCP endpoints are named ${r.id}`);
|
|
166
|
+
else
|
|
167
|
+
mcpEndpoints.set(r.id, r);
|
|
134
168
|
}
|
|
135
169
|
if (problems.length > 0)
|
|
136
170
|
throw new RegistrationError(problems);
|
|
137
|
-
return new Registry(components, endpoints);
|
|
171
|
+
return new Registry(components, endpoints, mcpEndpoints);
|
|
138
172
|
}
|
|
139
173
|
/** The discovery `Spec` for what is registered, without listening. */
|
|
140
174
|
spec() {
|
|
@@ -191,7 +225,18 @@ function registerEventSourced(cls, problems) {
|
|
|
191
225
|
const handlers = collectHandlers(cls.handlers, "handlers", ["command", "query"], fail);
|
|
192
226
|
if (!ok())
|
|
193
227
|
return undefined;
|
|
194
|
-
|
|
228
|
+
if (cls.replicated !== undefined && typeof cls.replicated !== "boolean")
|
|
229
|
+
fail(`replicated must be true or false, not ${String(cls.replicated)}`);
|
|
230
|
+
return Object.freeze({
|
|
231
|
+
kind: "event-sourced",
|
|
232
|
+
id: cls.componentId,
|
|
233
|
+
cls,
|
|
234
|
+
stateCodec: codecFor(cls.state),
|
|
235
|
+
eventCodec: codecFor(cls.events),
|
|
236
|
+
handlers,
|
|
237
|
+
snapshotEvery,
|
|
238
|
+
replicated: cls.replicated === true,
|
|
239
|
+
});
|
|
195
240
|
}
|
|
196
241
|
function registerKeyValue(cls, problems) {
|
|
197
242
|
const { fail, ok } = checker(cls, problems);
|
|
@@ -261,6 +306,12 @@ function topicProblems(cls, consumer, fail) {
|
|
|
261
306
|
if ((cls.contract !== undefined || cls.broker !== undefined || cls.parallel !== undefined) && !readsTopic) {
|
|
262
307
|
fail("declares a contract, a broker or parallel, which apply to a topic; it reads a component");
|
|
263
308
|
}
|
|
309
|
+
if (cls.project !== undefined && (typeof cls.project !== "string" || cls.project.trim() === ""))
|
|
310
|
+
fail("project must be the id of the project whose topic this is");
|
|
311
|
+
if (cls.project !== undefined && !readsTopic)
|
|
312
|
+
fail("names a project, which applies to a topic; it reads a component");
|
|
313
|
+
if (cls.project !== undefined && cls.broker !== undefined)
|
|
314
|
+
fail("names a project and a broker; another project's topic is on the installation's broker");
|
|
264
315
|
}
|
|
265
316
|
function sourceOf(cls, fail, consumer) {
|
|
266
317
|
const hasSource = cls.source !== undefined;
|
|
@@ -281,6 +332,7 @@ function sourceOf(cls, fail, consumer) {
|
|
|
281
332
|
...(cls.contract instanceof Contract ? { contract: cls.contract } : {}),
|
|
282
333
|
...(typeof cls.broker === "string" ? { broker: cls.broker } : {}),
|
|
283
334
|
...(cls.parallel === true ? { parallel: true } : {}),
|
|
335
|
+
...(typeof cls.project === "string" ? { project: cls.project } : {}),
|
|
284
336
|
};
|
|
285
337
|
}
|
|
286
338
|
const src = cls.source;
|
|
@@ -413,6 +465,7 @@ function registerConsumer(cls, problems) {
|
|
|
413
465
|
produces,
|
|
414
466
|
graph: false,
|
|
415
467
|
version: cls.version,
|
|
468
|
+
mirroredChanges: cls.mirroredChanges === true,
|
|
416
469
|
});
|
|
417
470
|
}
|
|
418
471
|
/**
|
|
@@ -436,10 +489,15 @@ function publicationOf(declared, fail) {
|
|
|
436
489
|
fail("producesTo.contract must be a Contract: Contract.fromFile(path, name)");
|
|
437
490
|
if (p.broker !== undefined && (typeof p.broker !== "string" || p.broker.trim() === ""))
|
|
438
491
|
fail("producesTo.broker must be the name of a broker the project declares");
|
|
492
|
+
if (p.project !== undefined && (typeof p.project !== "string" || p.project.trim() === ""))
|
|
493
|
+
fail("producesTo.project must be the id of the project whose topic this is");
|
|
494
|
+
if (p.project !== undefined && p.broker !== undefined)
|
|
495
|
+
fail("producesTo names a project and a broker; another project's topic is on the installation's broker");
|
|
439
496
|
return {
|
|
440
497
|
topic: p.topic,
|
|
441
498
|
...(p.contract instanceof Contract ? { contract: p.contract } : {}),
|
|
442
499
|
...(typeof p.broker === "string" ? { broker: p.broker } : {}),
|
|
500
|
+
...(typeof p.project === "string" ? { project: p.project } : {}),
|
|
443
501
|
};
|
|
444
502
|
}
|
|
445
503
|
/** A graph consumer is a consumer to discovery and to the runtime: one that produces deltas to its topic. */
|
|
@@ -465,6 +523,7 @@ function registerGraphConsumer(cls, problems) {
|
|
|
465
523
|
produces: undefined,
|
|
466
524
|
graph: true,
|
|
467
525
|
version: cls.version,
|
|
526
|
+
mirroredChanges: cls.mirroredChanges === true,
|
|
468
527
|
});
|
|
469
528
|
}
|
|
470
529
|
function registerTimedAction(cls, problems) {
|
|
@@ -610,9 +669,16 @@ function registerEndpoint(cls, problems) {
|
|
|
610
669
|
if (typeof cls.prefix !== "string" || !cls.prefix.startsWith("/") || (cls.prefix.length > 1 && cls.prefix.endsWith("/"))) {
|
|
611
670
|
fail(`needs a static prefix starting with "/" and not ending with one, not ${JSON.stringify(cls.prefix)}`);
|
|
612
671
|
}
|
|
672
|
+
else if ((cls.prefix === "/_ankka" || cls.prefix.startsWith("/_ankka/")) && cls._sdkReserved !== true) {
|
|
673
|
+
// The SDK's own directory-store endpoint is the one exception, as the sidecar admits it alone.
|
|
674
|
+
fail(`prefix '${cls.prefix}' is under /_ankka, which is the runtime's`);
|
|
675
|
+
}
|
|
613
676
|
if (!isAcl(cls.acl)) {
|
|
614
677
|
fail(`needs a static acl — Acl.allowAll, Acl.denyAll, Acl.authenticated or Acl.allowCallers(...). An unstated access rule is a decision nobody made`);
|
|
615
678
|
}
|
|
679
|
+
else if (!isEndpointAcl(cls.acl)) {
|
|
680
|
+
fail("admits client keys or states Acl.anyOf, which only an MCP endpoint can: extend McpEndpoint");
|
|
681
|
+
}
|
|
616
682
|
const routes = new Map();
|
|
617
683
|
const seen = new Map();
|
|
618
684
|
if (typeof cls.routes !== "object" || cls.routes === null) {
|
|
@@ -632,6 +698,8 @@ function registerEndpoint(cls, problems) {
|
|
|
632
698
|
seen.set(key, id);
|
|
633
699
|
if (r.acl !== undefined && !isAcl(r.acl))
|
|
634
700
|
fail(`routes.${id}: unknown acl ${JSON.stringify(r.acl)}`);
|
|
701
|
+
else if (r.acl !== undefined && !isEndpointAcl(r.acl))
|
|
702
|
+
fail(`routes.${id} admits client keys or states Acl.anyOf, which only an MCP endpoint can`);
|
|
635
703
|
routes.set(id, r);
|
|
636
704
|
}
|
|
637
705
|
}
|
package/dist/sigv4.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** A key pair. Never printed whole: `toString` hides the secret. */
|
|
2
|
+
export declare class Credentials {
|
|
3
|
+
#private;
|
|
4
|
+
readonly accessKey: string;
|
|
5
|
+
constructor(accessKey: string, secretKey: string);
|
|
6
|
+
/** @internal */
|
|
7
|
+
get _secretKey(): string;
|
|
8
|
+
toString(): string;
|
|
9
|
+
toJSON(): string;
|
|
10
|
+
}
|
|
11
|
+
export declare const ALGORITHM = "AWS4-HMAC-SHA256";
|
|
12
|
+
/** The payload hash a put sends so its body is streamed once and never hashed. */
|
|
13
|
+
export declare const UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD";
|
|
14
|
+
export declare function sha256Hex(data: Uint8Array | string): string;
|
|
15
|
+
export declare function hmac(key: Uint8Array | string, data: string): Buffer;
|
|
16
|
+
/** The hash of an empty payload. */
|
|
17
|
+
export declare const EMPTY_PAYLOAD: string;
|
|
18
|
+
/** RFC 3986 encoding as SigV4 states it: unreserved characters kept, everything else `%XX` of its UTF-8 bytes. */
|
|
19
|
+
export declare function encode(text: string, encodeSlash: boolean): string;
|
|
20
|
+
/** One request: the endpoint's scheme and authority, a decoded path and decoded query pairs. */
|
|
21
|
+
export declare class SignedRequest {
|
|
22
|
+
readonly method: string;
|
|
23
|
+
readonly endpoint: URL;
|
|
24
|
+
readonly path: string;
|
|
25
|
+
readonly query: readonly (readonly [string, string])[];
|
|
26
|
+
constructor(method: string, endpoint: URL | string, path: string, query?: readonly (readonly [string, string])[]);
|
|
27
|
+
/** The `host` header's value: the host, with the port when the endpoint names one. */
|
|
28
|
+
get host(): string;
|
|
29
|
+
get canonicalPath(): string;
|
|
30
|
+
get canonicalQuery(): string;
|
|
31
|
+
/** The URL the request is sent to: the same encoding as the one signed. */
|
|
32
|
+
get url(): string;
|
|
33
|
+
withQuery(more: readonly (readonly [string, string])[]): SignedRequest;
|
|
34
|
+
}
|
|
35
|
+
/** `20150830T123600Z`. */
|
|
36
|
+
export declare function amzDate(now: Date): string;
|
|
37
|
+
/**
|
|
38
|
+
* The headers to send with `request` so the store believes it: `x-amz-date` and `Authorization`.
|
|
39
|
+
* `headers` are the ones to sign beside `host` and `x-amz-date` — an S3 request includes
|
|
40
|
+
* `x-amz-content-sha256` among them — and are sent as given.
|
|
41
|
+
*/
|
|
42
|
+
export declare function sign(request: SignedRequest, headers: Readonly<Record<string, string>>, payloadHash: string, credentials: Credentials, region: string, service: string, now: Date): {
|
|
43
|
+
"x-amz-date": string;
|
|
44
|
+
Authorization: string;
|
|
45
|
+
};
|
|
46
|
+
/** A URL anyone may use, until it expires, for `request`'s method on its path. Signs `host` alone. */
|
|
47
|
+
export declare function presign(request: SignedRequest, credentials: Credentials, region: string, service: string, expiresSeconds: number, now: Date): string;
|
package/dist/sigv4.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// AWS Signature Version 4, as S3 and every store that speaks it reads it: a request signed in its
|
|
2
|
+
// headers, and a URL signed in its query string (feature 055). The runtime's `SigV4.scala` made the
|
|
3
|
+
// same choices, and the same vectors hold both: paths are encoded once, segment by segment, as S3
|
|
4
|
+
// expects; a put's payload is `UNSIGNED-PAYLOAD`, so a stream is sent once and never hashed.
|
|
5
|
+
//
|
|
6
|
+
// The signer works on a request's parts — its method, its endpoint, its path and query *before*
|
|
7
|
+
// encoding, the headers it signs and the hash of its payload — so the header form and the query form
|
|
8
|
+
// share one canonical request, and AWS's published examples test it with no client at all.
|
|
9
|
+
import { createHash, createHmac } from "node:crypto";
|
|
10
|
+
/** A key pair. Never printed whole: `toString` hides the secret. */
|
|
11
|
+
export class Credentials {
|
|
12
|
+
accessKey;
|
|
13
|
+
#secretKey;
|
|
14
|
+
constructor(accessKey, secretKey) {
|
|
15
|
+
this.accessKey = accessKey;
|
|
16
|
+
this.#secretKey = secretKey;
|
|
17
|
+
Object.freeze(this);
|
|
18
|
+
}
|
|
19
|
+
/** @internal */
|
|
20
|
+
get _secretKey() {
|
|
21
|
+
return this.#secretKey;
|
|
22
|
+
}
|
|
23
|
+
toString() {
|
|
24
|
+
return `Credentials(${this.accessKey}, ***)`;
|
|
25
|
+
}
|
|
26
|
+
toJSON() {
|
|
27
|
+
return this.toString();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export const ALGORITHM = "AWS4-HMAC-SHA256";
|
|
31
|
+
/** The payload hash a put sends so its body is streamed once and never hashed. */
|
|
32
|
+
export const UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD";
|
|
33
|
+
const utf8 = new TextEncoder();
|
|
34
|
+
export function sha256Hex(data) {
|
|
35
|
+
return createHash("sha256").update(typeof data === "string" ? utf8.encode(data) : data).digest("hex");
|
|
36
|
+
}
|
|
37
|
+
export function hmac(key, data) {
|
|
38
|
+
return createHmac("sha256", key).update(data, "utf8").digest();
|
|
39
|
+
}
|
|
40
|
+
/** The hash of an empty payload. */
|
|
41
|
+
export const EMPTY_PAYLOAD = sha256Hex(new Uint8Array());
|
|
42
|
+
/** RFC 3986 encoding as SigV4 states it: unreserved characters kept, everything else `%XX` of its UTF-8 bytes. */
|
|
43
|
+
export function encode(text, encodeSlash) {
|
|
44
|
+
let out = "";
|
|
45
|
+
for (const b of utf8.encode(text)) {
|
|
46
|
+
const c = String.fromCharCode(b);
|
|
47
|
+
if ((b >= 0x41 && b <= 0x5a) || (b >= 0x61 && b <= 0x7a) || (b >= 0x30 && b <= 0x39) || c === "-" || c === "_" || c === "." || c === "~" || (c === "/" && !encodeSlash)) {
|
|
48
|
+
out += c;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
out += "%" + b.toString(16).toUpperCase().padStart(2, "0");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
/** One request: the endpoint's scheme and authority, a decoded path and decoded query pairs. */
|
|
57
|
+
export class SignedRequest {
|
|
58
|
+
method;
|
|
59
|
+
endpoint;
|
|
60
|
+
path;
|
|
61
|
+
query;
|
|
62
|
+
constructor(method, endpoint, path, query = []) {
|
|
63
|
+
this.method = method;
|
|
64
|
+
this.endpoint = typeof endpoint === "string" ? new URL(endpoint) : endpoint;
|
|
65
|
+
this.path = path;
|
|
66
|
+
this.query = query;
|
|
67
|
+
Object.freeze(this);
|
|
68
|
+
}
|
|
69
|
+
/** The `host` header's value: the host, with the port when the endpoint names one. */
|
|
70
|
+
get host() {
|
|
71
|
+
return this.endpoint.port === "" ? this.endpoint.hostname : `${this.endpoint.hostname}:${this.endpoint.port}`;
|
|
72
|
+
}
|
|
73
|
+
get canonicalPath() {
|
|
74
|
+
const raw = this.endpoint.pathname === "/" ? "" : this.endpoint.pathname;
|
|
75
|
+
const base = raw.endsWith("/") ? raw.slice(0, -1) : raw;
|
|
76
|
+
const p = this.path.startsWith("/") ? this.path : "/" + this.path;
|
|
77
|
+
return base + encode(p, false);
|
|
78
|
+
}
|
|
79
|
+
get canonicalQuery() {
|
|
80
|
+
return this.query
|
|
81
|
+
.map(([k, v]) => [encode(k, true), encode(v, true)])
|
|
82
|
+
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0))
|
|
83
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
84
|
+
.join("&");
|
|
85
|
+
}
|
|
86
|
+
/** The URL the request is sent to: the same encoding as the one signed. */
|
|
87
|
+
get url() {
|
|
88
|
+
const q = this.canonicalQuery;
|
|
89
|
+
return `${this.endpoint.protocol}//${this.host}${this.canonicalPath}${q === "" ? "" : "?" + q}`;
|
|
90
|
+
}
|
|
91
|
+
withQuery(more) {
|
|
92
|
+
return new SignedRequest(this.method, this.endpoint, this.path, [...this.query, ...more]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function pad(n, width = 2) {
|
|
96
|
+
return String(n).padStart(width, "0");
|
|
97
|
+
}
|
|
98
|
+
/** `20150830T123600Z`. */
|
|
99
|
+
export function amzDate(now) {
|
|
100
|
+
return `${shortDate(now)}T${pad(now.getUTCHours())}${pad(now.getUTCMinutes())}${pad(now.getUTCSeconds())}Z`;
|
|
101
|
+
}
|
|
102
|
+
function shortDate(now) {
|
|
103
|
+
return `${pad(now.getUTCFullYear(), 4)}${pad(now.getUTCMonth() + 1)}${pad(now.getUTCDate())}`;
|
|
104
|
+
}
|
|
105
|
+
function signingKey(secret, now, region, service) {
|
|
106
|
+
const date = hmac(utf8.encode("AWS4" + secret), shortDate(now));
|
|
107
|
+
return hmac(hmac(hmac(date, region), service), "aws4_request");
|
|
108
|
+
}
|
|
109
|
+
function stringToSign(stamp, scope, canonicalRequest) {
|
|
110
|
+
return `${ALGORITHM}\n${stamp}\n${scope}\n${sha256Hex(canonicalRequest)}`;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The headers to send with `request` so the store believes it: `x-amz-date` and `Authorization`.
|
|
114
|
+
* `headers` are the ones to sign beside `host` and `x-amz-date` — an S3 request includes
|
|
115
|
+
* `x-amz-content-sha256` among them — and are sent as given.
|
|
116
|
+
*/
|
|
117
|
+
export function sign(request, headers, payloadHash, credentials, region, service, now) {
|
|
118
|
+
const stamp = amzDate(now);
|
|
119
|
+
const signed = new Map();
|
|
120
|
+
for (const [k, v] of Object.entries(headers))
|
|
121
|
+
signed.set(k.toLowerCase(), v);
|
|
122
|
+
signed.set("host", request.host);
|
|
123
|
+
signed.set("x-amz-date", stamp);
|
|
124
|
+
const sorted = [...signed.entries()].map(([k, v]) => [k, v.trim().replace(/ +/g, " ")]).sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
125
|
+
const names = sorted.map(([k]) => k).join(";");
|
|
126
|
+
const canonical = sorted.map(([k, v]) => `${k}:${v}\n`).join("");
|
|
127
|
+
const scope = `${shortDate(now)}/${region}/${service}/aws4_request`;
|
|
128
|
+
const creq = [request.method, request.canonicalPath, request.canonicalQuery, canonical, names, payloadHash].join("\n");
|
|
129
|
+
const signature = hmac(signingKey(credentials._secretKey, now, region, service), stringToSign(stamp, scope, creq)).toString("hex");
|
|
130
|
+
return {
|
|
131
|
+
"x-amz-date": stamp,
|
|
132
|
+
Authorization: `${ALGORITHM} Credential=${credentials.accessKey}/${scope}, SignedHeaders=${names}, Signature=${signature}`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** A URL anyone may use, until it expires, for `request`'s method on its path. Signs `host` alone. */
|
|
136
|
+
export function presign(request, credentials, region, service, expiresSeconds, now) {
|
|
137
|
+
const stamp = amzDate(now);
|
|
138
|
+
const scope = `${shortDate(now)}/${region}/${service}/aws4_request`;
|
|
139
|
+
const withAuth = request.withQuery([
|
|
140
|
+
["X-Amz-Algorithm", ALGORITHM],
|
|
141
|
+
["X-Amz-Credential", `${credentials.accessKey}/${scope}`],
|
|
142
|
+
["X-Amz-Date", stamp],
|
|
143
|
+
["X-Amz-Expires", String(expiresSeconds)],
|
|
144
|
+
["X-Amz-SignedHeaders", "host"],
|
|
145
|
+
]);
|
|
146
|
+
const creq = [request.method, withAuth.canonicalPath, withAuth.canonicalQuery, `host:${request.host}\n`, "host", UNSIGNED_PAYLOAD].join("\n");
|
|
147
|
+
const signature = hmac(signingKey(credentials._secretKey, now, region, service), stringToSign(stamp, scope, creq)).toString("hex");
|
|
148
|
+
return withAuth.withQuery([["X-Amz-Signature", signature]]).url;
|
|
149
|
+
}
|