ankka 0.15.0 → 0.16.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/dist/_proto/ankka/protocol/v1/client_pb.d.ts +365 -0
- package/dist/_proto/ankka/protocol/v1/client_pb.js +61 -5
- package/dist/_proto/ankka/protocol/v1/discovery_pb.d.ts +7 -0
- package/dist/_proto/ankka/protocol/v1/discovery_pb.js +1 -1
- package/dist/_proto/ankka/protocol/v1/erasure_pb.d.ts +72 -0
- package/dist/_proto/ankka/protocol/v1/erasure_pb.js +24 -0
- package/dist/_proto/ankka/protocol/v1/event_sourced_pb.d.ts +14 -0
- package/dist/_proto/ankka/protocol/v1/event_sourced_pb.js +1 -1
- package/dist/_proto/ankka/protocol/v1/wasm_pb.d.ts +136 -1
- package/dist/_proto/ankka/protocol/v1/wasm_pb.js +21 -1
- package/dist/autonomous.d.ts +12 -2
- package/dist/autonomous.js +17 -3
- package/dist/client.d.ts +21 -3
- package/dist/client.js +46 -13
- package/dist/codec.d.ts +2 -0
- package/dist/codec.js +8 -1
- package/dist/consumer.d.ts +5 -0
- package/dist/consumer.js +10 -0
- package/dist/endpoint.d.ts +5 -0
- package/dist/endpoint.js +10 -0
- package/dist/erasure.d.ts +35 -0
- package/dist/erasure.js +10 -0
- package/dist/erasures.d.ts +32 -0
- package/dist/erasures.js +65 -0
- package/dist/graph.d.ts +5 -0
- package/dist/graph.js +10 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/json.js +110 -0
- package/dist/keyring.d.ts +119 -0
- package/dist/keyring.js +417 -0
- package/dist/personal.d.ts +97 -0
- package/dist/personal.js +209 -0
- package/dist/schema.d.ts +13 -0
- package/dist/schema.js +43 -0
- package/dist/server/agent.js +2 -1
- package/dist/server/discovery.d.ts +13 -1
- package/dist/server/discovery.js +14 -2
- package/dist/server/erasure.d.ts +7 -0
- package/dist/server/erasure.js +40 -0
- package/dist/server/eventSourced.js +25 -8
- package/dist/server/keyValue.js +6 -2
- package/dist/server/payloads.d.ts +10 -2
- package/dist/server/payloads.js +36 -4
- package/dist/server/server.d.ts +2 -0
- package/dist/server/server.js +59 -2
- package/dist/server/stateless.js +10 -1
- package/dist/server/workflow.js +8 -2
- package/dist/service.d.ts +12 -1
- package/dist/service.js +60 -3
- package/dist/spec.d.ts +1 -1
- package/dist/spec.js +2 -1
- package/dist/storage.d.ts +48 -2
- package/dist/storage.js +138 -7
- package/dist/testkit/index.d.ts +1 -1
- package/dist/testkit/integration.d.ts +35 -0
- package/dist/testkit/integration.js +106 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/consumer.js
CHANGED
|
@@ -3,6 +3,7 @@ import { secretsFor } from "./client.js";
|
|
|
3
3
|
import { processStorage } from "./storage.js";
|
|
4
4
|
import { ConsumerEffects } from "./effects/stateless.js";
|
|
5
5
|
import { servicesFor } from "./services.js";
|
|
6
|
+
import { erasuresFor } from "./erasures.js";
|
|
6
7
|
/** The metadata entry a runtime sets on a consumer's request to say what it speaks: its protocol version. */
|
|
7
8
|
export const PROTOCOL_KEY = "ankka.protocol";
|
|
8
9
|
/** Thrown when a consumer answers with several messages, or a record key, to a runtime that would drop them. */
|
|
@@ -89,6 +90,15 @@ export class Consumer {
|
|
|
89
90
|
set services(services) {
|
|
90
91
|
this.#services = services;
|
|
91
92
|
}
|
|
93
|
+
#erasures;
|
|
94
|
+
/** Asks for erasure requests as this service (1.23), which needs the project's `erasure` grant. */
|
|
95
|
+
get erasures() {
|
|
96
|
+
return this.#erasures ?? erasuresFor(this.client);
|
|
97
|
+
}
|
|
98
|
+
/** A unit test's in place of the runtime's: `component.erasures = new Erasures(client)`. */
|
|
99
|
+
set erasures(erasures) {
|
|
100
|
+
this.#erasures = erasures;
|
|
101
|
+
}
|
|
92
102
|
/** @internal */
|
|
93
103
|
get _kind() {
|
|
94
104
|
return "consumer";
|
package/dist/endpoint.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { type Buckets } from "./storage.ts";
|
|
|
4
4
|
import { type RequestContext } from "./context.ts";
|
|
5
5
|
import type { Acl, RouteTable } from "./routes.ts";
|
|
6
6
|
import { type Services } from "./services.ts";
|
|
7
|
+
import { type Erasures } from "./erasures.ts";
|
|
7
8
|
export declare abstract class Endpoint {
|
|
8
9
|
#private;
|
|
9
10
|
/** The request being handled: path parameters, query, headers, principal. Request-scoped. */
|
|
@@ -22,6 +23,10 @@ export declare abstract class Endpoint {
|
|
|
22
23
|
get services(): Services;
|
|
23
24
|
/** A unit test's in place of the runtime's: `component.services = new ScriptedServices()`. */
|
|
24
25
|
set services(services: Services);
|
|
26
|
+
/** Asks for erasure requests as this service (1.23), which needs the project's `erasure` grant. */
|
|
27
|
+
get erasures(): Erasures;
|
|
28
|
+
/** A unit test's in place of the runtime's: `component.erasures = new Erasures(client)`. */
|
|
29
|
+
set erasures(erasures: Erasures);
|
|
25
30
|
/** The service's client keys (1.20): issued for its MCP endpoint, kept by the runtime, each shown once. */
|
|
26
31
|
get clientKeys(): ClientKeys;
|
|
27
32
|
/** A unit test's in place of the runtime's: `component.clientKeys = new InMemoryClientKeys()`. */
|
package/dist/endpoint.js
CHANGED
|
@@ -13,6 +13,7 @@ import { secretsFor } from "./client.js";
|
|
|
13
13
|
import { processStorage } from "./storage.js";
|
|
14
14
|
import { currentRequest, requestIfAny } from "./context.js";
|
|
15
15
|
import { servicesFor } from "./services.js";
|
|
16
|
+
import { erasuresFor } from "./erasures.js";
|
|
16
17
|
export class Endpoint {
|
|
17
18
|
#client;
|
|
18
19
|
/** The request being handled: path parameters, query, headers, principal. Request-scoped. */
|
|
@@ -53,6 +54,15 @@ export class Endpoint {
|
|
|
53
54
|
set services(services) {
|
|
54
55
|
this.#services = services;
|
|
55
56
|
}
|
|
57
|
+
#erasures;
|
|
58
|
+
/** Asks for erasure requests as this service (1.23), which needs the project's `erasure` grant. */
|
|
59
|
+
get erasures() {
|
|
60
|
+
return this.#erasures ?? erasuresFor(this.client);
|
|
61
|
+
}
|
|
62
|
+
/** A unit test's in place of the runtime's: `component.erasures = new Erasures(client)`. */
|
|
63
|
+
set erasures(erasures) {
|
|
64
|
+
this.#erasures = erasures;
|
|
65
|
+
}
|
|
56
66
|
#clientKeys;
|
|
57
67
|
/** The service's client keys (1.20): issued for its MCP endpoint, kept by the runtime, each shown once. */
|
|
58
68
|
get clientKeys() {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ComponentClient, Secrets } from "./client.ts";
|
|
2
|
+
import type { Metadata } from "./effects/common.ts";
|
|
3
|
+
import type { Erasures } from "./erasures.ts";
|
|
4
|
+
import type { DataSubject } from "./personal.ts";
|
|
5
|
+
import type { Buckets } from "./storage.ts";
|
|
6
|
+
import type { Duration } from "./time.ts";
|
|
7
|
+
/** What a handler is given beside the subject. */
|
|
8
|
+
export interface ErasureContext {
|
|
9
|
+
readonly client: ComponentClient;
|
|
10
|
+
readonly secrets: Secrets;
|
|
11
|
+
/** The service's buckets. */
|
|
12
|
+
readonly storage: Buckets;
|
|
13
|
+
/** Asks for erasure requests as this service (1.23). */
|
|
14
|
+
readonly erasures: Erasures;
|
|
15
|
+
/** The erasure's sequence number in the keyring. */
|
|
16
|
+
readonly erasureSeq: bigint;
|
|
17
|
+
/** Whether this is a re-run of a handler that already ran for this subject. */
|
|
18
|
+
readonly rerun: boolean;
|
|
19
|
+
readonly metadata: Metadata;
|
|
20
|
+
}
|
|
21
|
+
/** When what the handler erased is gone: at once, or after the store's own retention. */
|
|
22
|
+
export type Finality = {
|
|
23
|
+
readonly kind: "immediate";
|
|
24
|
+
} | {
|
|
25
|
+
readonly kind: "after";
|
|
26
|
+
readonly after: Duration;
|
|
27
|
+
};
|
|
28
|
+
/** What a handler reports: how many objects it erased, and when they are gone. */
|
|
29
|
+
export interface ErasureReport {
|
|
30
|
+
readonly objectsErased?: number | bigint;
|
|
31
|
+
readonly finality?: Finality;
|
|
32
|
+
}
|
|
33
|
+
export type ErasureHandler = (ctx: ErasureContext, subject: DataSubject) => Promise<ErasureReport | void> | ErasureReport | void;
|
|
34
|
+
/** The finality as the protocol carries it: `immediate`, or `after:` and an ISO-8601 duration. */
|
|
35
|
+
export declare function finalityToProto(finality: Finality | undefined): string;
|
package/dist/erasure.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The erasure handler (feature 042): what the platform cannot erase for the service, such as objects in
|
|
2
|
+
// a bucket. `service.erasureHandler(async (ctx, subject) => …)`; one per service. The sidecar calls it
|
|
3
|
+
// for each erased data subject and again on each re-run within the window, so it must be idempotent. A
|
|
4
|
+
// handler that throws is recorded as failed for that run and run again on the next.
|
|
5
|
+
/** The finality as the protocol carries it: `immediate`, or `after:` and an ISO-8601 duration. */
|
|
6
|
+
export function finalityToProto(finality) {
|
|
7
|
+
if (finality === undefined || finality.kind === "immediate")
|
|
8
|
+
return "immediate";
|
|
9
|
+
return `after:${finality.after.toString()}`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type ComponentClient } from "./client.ts";
|
|
2
|
+
import type { LocalDate } from "./time.ts";
|
|
3
|
+
/** The first protocol whose sidecar asks for an erasure request; an older one answers `UNIMPLEMENTED`. */
|
|
4
|
+
export declare const ERASURES_SINCE = "1.23";
|
|
5
|
+
/** What to ask: whose personal data, where, and from when. */
|
|
6
|
+
export interface AskErasure {
|
|
7
|
+
readonly subject: string;
|
|
8
|
+
/** The project to erase in; this service's own when absent. */
|
|
9
|
+
readonly project?: string;
|
|
10
|
+
/** Held until this date; sent as ISO-8601 (`2027-01-31`). */
|
|
11
|
+
readonly notBefore?: LocalDate;
|
|
12
|
+
readonly reason?: string;
|
|
13
|
+
readonly correlation?: string;
|
|
14
|
+
}
|
|
15
|
+
/** An erasure request as asked: its id in its project and its state (held, applying, applied). */
|
|
16
|
+
export interface ErasureAsked {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly state: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Asks for erasure requests as this service. A refusal is a `CommandError` with the control plane's
|
|
22
|
+
* code: `FORBIDDEN` without the `erasure` grant, `CONFLICT` while erasure is off in the project,
|
|
23
|
+
* `BAD_REQUEST` for a subject outside the rule.
|
|
24
|
+
*/
|
|
25
|
+
export declare class Erasures {
|
|
26
|
+
#private;
|
|
27
|
+
/** Asked through `client`'s sidecar; with none, every ask throws. */
|
|
28
|
+
constructor(client?: ComponentClient);
|
|
29
|
+
ask(request: AskErasure): Promise<ErasureAsked>;
|
|
30
|
+
}
|
|
31
|
+
/** @internal The erasures reached through `client`'s sidecar. */
|
|
32
|
+
export declare function erasuresFor(client: ComponentClient): Erasures;
|
package/dist/erasures.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Asking for an erasure request from code (feature 042, protocol 1.23): `this.erasures.ask({ subject })`
|
|
2
|
+
// in an endpoint or a consumer, `ctx.erasures.ask(...)` in the erasure handler. The request is asked of
|
|
3
|
+
// the control plane as this service, through the sidecar's `Client.AskErasure`; the service needs the
|
|
4
|
+
// `erasure` grant of the project it asks in, its own included, and is named as who asked.
|
|
5
|
+
//
|
|
6
|
+
// const asked = await this.erasures.ask({ subject: "customer-42", notBefore: LocalDate.of(2027, 1, 31), reason: "aml-retention" })
|
|
7
|
+
// asked.id, asked.state // "held", "applying" or "applied"
|
|
8
|
+
import { Code, ConnectError } from "@connectrpc/connect";
|
|
9
|
+
import { channelOf } from "./client.js";
|
|
10
|
+
import { metadataToProto } from "./context.js";
|
|
11
|
+
import { CommandError, ErrorCode } from "./effects/common.js";
|
|
12
|
+
import { errorCodeFromProto } from "./kinds.js";
|
|
13
|
+
import { PROTOCOL_VERSION } from "./spec.js";
|
|
14
|
+
/** The first protocol whose sidecar asks for an erasure request; an older one answers `UNIMPLEMENTED`. */
|
|
15
|
+
export const ERASURES_SINCE = "1.23";
|
|
16
|
+
/**
|
|
17
|
+
* Asks for erasure requests as this service. A refusal is a `CommandError` with the control plane's
|
|
18
|
+
* code: `FORBIDDEN` without the `erasure` grant, `CONFLICT` while erasure is off in the project,
|
|
19
|
+
* `BAD_REQUEST` for a subject outside the rule.
|
|
20
|
+
*/
|
|
21
|
+
export class Erasures {
|
|
22
|
+
#client;
|
|
23
|
+
/** Asked through `client`'s sidecar; with none, every ask throws. */
|
|
24
|
+
constructor(client) {
|
|
25
|
+
this.#client = client;
|
|
26
|
+
}
|
|
27
|
+
async ask(request) {
|
|
28
|
+
const channel = this.#client ? channelOf(this.#client) : undefined;
|
|
29
|
+
if (!channel)
|
|
30
|
+
throw new Error("these erasures are not connected to a runtime; a unit test assigns its own");
|
|
31
|
+
const reply = await channel
|
|
32
|
+
.stub()
|
|
33
|
+
.askErasure({
|
|
34
|
+
project: request.project ?? "",
|
|
35
|
+
subject: request.subject,
|
|
36
|
+
...(request.notBefore !== undefined ? { notBefore: request.notBefore.toString() } : {}),
|
|
37
|
+
...(request.reason !== undefined ? { reason: request.reason } : {}),
|
|
38
|
+
...(request.correlation !== undefined ? { correlation: request.correlation } : {}),
|
|
39
|
+
metadata: metadataToProto(channel.metadata),
|
|
40
|
+
})
|
|
41
|
+
.catch((failure) => {
|
|
42
|
+
if (failure instanceof ConnectError && failure.code === Code.Unimplemented) {
|
|
43
|
+
throw new CommandError({
|
|
44
|
+
message: `the runtime beside this process cannot ask for an erasure, which needs protocol ${ERASURES_SINCE}; this process speaks ${PROTOCOL_VERSION}: ${failure.rawMessage}`,
|
|
45
|
+
code: ErrorCode.Internal,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
throw failure;
|
|
49
|
+
});
|
|
50
|
+
switch (reply.reply.case) {
|
|
51
|
+
case "ok":
|
|
52
|
+
return Object.freeze({ id: reply.reply.value.id, state: reply.reply.value.state });
|
|
53
|
+
case "error": {
|
|
54
|
+
const error = reply.reply.value;
|
|
55
|
+
throw new CommandError({ message: error.message, code: errorCodeFromProto(error.code), details: { ...(error.details ?? {}) } });
|
|
56
|
+
}
|
|
57
|
+
default:
|
|
58
|
+
throw new CommandError({ message: "the runtime answered an erasure request with nothing", code: ErrorCode.Internal });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** @internal The erasures reached through `client`'s sidecar. */
|
|
63
|
+
export function erasuresFor(client) {
|
|
64
|
+
return new Erasures(client);
|
|
65
|
+
}
|
package/dist/graph.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { EffectLike, Metadata } from "./effects/common.ts";
|
|
|
4
4
|
import { type ComponentClient, type ComponentRef, type Secrets } from "./client.ts";
|
|
5
5
|
import { type Buckets } from "./storage.ts";
|
|
6
6
|
import { type Services } from "./services.ts";
|
|
7
|
+
import { type Erasures } from "./erasures.ts";
|
|
7
8
|
import type { Standing } from "./standing.ts";
|
|
8
9
|
type MaybePromise<T> = T | Promise<T>;
|
|
9
10
|
/** The contract's name: the manifest and the `ce-type` of every delta. */
|
|
@@ -155,6 +156,10 @@ export declare abstract class GraphConsumer<M> {
|
|
|
155
156
|
get services(): Services;
|
|
156
157
|
/** A unit test's in place of the runtime's: `component.services = new ScriptedServices()`. */
|
|
157
158
|
set services(services: Services);
|
|
159
|
+
/** Asks for erasure requests as this service (1.23), which needs the project's `erasure` grant. */
|
|
160
|
+
get erasures(): Erasures;
|
|
161
|
+
/** A unit test's in place of the runtime's: `component.erasures = new Erasures(client)`. */
|
|
162
|
+
set erasures(erasures: Erasures);
|
|
158
163
|
/** @internal */
|
|
159
164
|
get _kind(): "consumer";
|
|
160
165
|
abstract onMessage(message: M): MaybePromise<GraphEffect>;
|
package/dist/graph.js
CHANGED
|
@@ -20,6 +20,7 @@ import { processStorage } from "./storage.js";
|
|
|
20
20
|
import { sequenceNumberOf } from "./consumer.js";
|
|
21
21
|
import { renderDouble, reviver } from "./json.js";
|
|
22
22
|
import { servicesFor } from "./services.js";
|
|
23
|
+
import { erasuresFor } from "./erasures.js";
|
|
23
24
|
/** The contract's name: the manifest and the `ce-type` of every delta. */
|
|
24
25
|
export const GRAPH_DELTA_SCHEMA = "ankka.graph-delta.v1";
|
|
25
26
|
/** The record key of every delta for the node `id`. */
|
|
@@ -445,6 +446,15 @@ export class GraphConsumer {
|
|
|
445
446
|
set services(services) {
|
|
446
447
|
this.#services = services;
|
|
447
448
|
}
|
|
449
|
+
#erasures;
|
|
450
|
+
/** Asks for erasure requests as this service (1.23), which needs the project's `erasure` grant. */
|
|
451
|
+
get erasures() {
|
|
452
|
+
return this.#erasures ?? erasuresFor(this.client);
|
|
453
|
+
}
|
|
454
|
+
/** A unit test's in place of the runtime's: `component.erasures = new Erasures(client)`. */
|
|
455
|
+
set erasures(erasures) {
|
|
456
|
+
this.#erasures = erasures;
|
|
457
|
+
}
|
|
448
458
|
/** @internal */
|
|
449
459
|
get _kind() {
|
|
450
460
|
return "consumer";
|
package/dist/index.d.ts
CHANGED
|
@@ -27,11 +27,12 @@ export { AutonomousAgent, AutonomousAgentCalls, TaskCalls, Tasks, accepted, reje
|
|
|
27
27
|
export { Endpoint, type EndpointClass } from "./endpoint.ts";
|
|
28
28
|
export { McpEndpoint, ToolTable, tools, MCP_ENDPOINTS_SINCE, type McpEndpointClass, type DeclaredTool, type ToolOptions, type ReferenceOptions, type RegisteredMcpEndpoint, type RegisteredMcpTool, type ToolRun, } from "./mcp-endpoint.ts";
|
|
29
29
|
export { ClientKeys, InMemoryClientKeys, Expiry, CLIENT_KEYS_SINCE, type IssueClientKey, type IssuedClientKey, type ClientKeySummary, } from "./client-keys.ts";
|
|
30
|
+
export { Erasures, ERASURES_SINCE, type AskErasure, type ErasureAsked } from "./erasures.ts";
|
|
30
31
|
export { type Socket, SocketClosed } from "./socket.ts";
|
|
31
32
|
export { Acl, Callers, type CallerMatcher, type CallersAcl, type AnyOfAcl, HttpProblem, get, post, put, patch, del, sse, sseEvent, sseEvents, type SseEvent, socket, type RouteRef, type RouteOptions, type RouteTable, type Params, type ParamNames, type HttpMethod } from "./routes.ts";
|
|
32
33
|
export { type CommandContext, type EventContext, type RequestContext, type Principal, type Caller, Query, Headers } from "./context.ts";
|
|
33
34
|
export { ComponentClient, Calls, TypedCalls, Invocation, AwaitingInvocation, AWAIT_SINCE, awaitPartJson, Views, Timers, Secrets, InMemorySecrets, noSecrets, WatchEnded, VIEW_STREAMS_SINCE, type AwaitPart, type WatchEvent, type WatchOptions, type Overflow, type TimerTarget, type TimedActionTarget, type ComponentRef } from "./client.ts";
|
|
34
|
-
export { ObjectRules, StorageRefusals, InMemoryBuckets, DirectoryBuckets, S3Buckets, S3Bucket, noStorage, chooseStorage, declaredBuckets, processStorage, s3Settings, bucketNameOf, bucketVariableSuffix, localAddress, STORAGE_PATH, type Buckets, type Bucket, type BucketExposure, type SignedAccess, type StoredObject, type ObjectInfo, type ObjectEntry, type PutOptions, type InMemoryBucketsOptions, type DirectoryBucketsOptions, type DeclaredBuckets, type S3Settings, type S3Target, type NamedTarget, type StorageBackend, type ChosenStorage, type StorageChoiceOptions, } from "./storage.ts";
|
|
35
|
+
export { ObjectRules, StorageRefusals, InMemoryBuckets, DirectoryBuckets, S3Buckets, S3Bucket, noStorage, chooseStorage, declaredBuckets, processStorage, s3Settings, bucketNameOf, bucketVariableSuffix, localAddress, STORAGE_PATH, subjectPrefix, parseVersions, type ErasedObjects, type VersionsPage, type Buckets, type Bucket, type BucketExposure, type SignedAccess, type StoredObject, type ObjectInfo, type ObjectEntry, type PutOptions, type InMemoryBucketsOptions, type DirectoryBucketsOptions, type DeclaredBuckets, type S3Settings, type S3Target, type NamedTarget, type StorageBackend, type ChosenStorage, type StorageChoiceOptions, } from "./storage.ts";
|
|
35
36
|
export { Credentials, SignedRequest, sign as signRequest, presign as presignUrl } from "./sigv4.ts";
|
|
36
37
|
export { Services, ServiceClient, ScriptedServices, noServices, ServiceError, ServiceUnresolvable, ServiceIdentityMismatch, ServiceUnanswered, ServiceCallFailed, type ServiceResponse, type ServiceRequest, type ServiceHeaders, } from "./services.ts";
|
|
37
38
|
export { Ankka, ServiceBuilder, Registry, RegistrationError, type ServiceOptions, type RegisteredComponent, type RegisteredEndpoint, type Source, type RegisteredEventSourced, type RegisteredKeyValue, type RegisteredWorkflow, type RegisteredView, type RegisteredKeyedView, type RegisteredKeyedSource, type RegisteredConsumer, type RegisteredTimedAction, type RegisteredAgent, } from "./service.ts";
|
|
@@ -43,3 +44,6 @@ export { StartFrom } from "./startFrom.ts";
|
|
|
43
44
|
export { isTerminal, isUnknown, type Standing, type StandingStatus } from "./standing.ts";
|
|
44
45
|
export { materialiseEventSourced, materialiseKeyValue, materialiseWorkflowCommand, materialiseStep, type Materialised, type MaterialisedKeyValue, type MaterialisedWorkflowCommand, type MaterialisedStep, } from "./materialise.ts";
|
|
45
46
|
export type { ComponentKind } from "./kinds.ts";
|
|
47
|
+
export { personal, subject, subjectProblem, isPersonal, Present, Erased, PersonalError, DATA_SUBJECT_RULE, type Personal, type DataSubject, type PersonalReason, } from "./personal.ts";
|
|
48
|
+
export { Keyring, currentKeyring, keyringSettings, DEFAULT_KEYRING_SETTINGS, type KeyAnswer, type KeyFetch, type KeyReply, type KeyringOptions, type KeyringSettings } from "./keyring.ts";
|
|
49
|
+
export { finalityToProto, type ErasureContext, type ErasureHandler, type ErasureReport, type Finality } from "./erasure.ts";
|
package/dist/index.js
CHANGED
|
@@ -38,11 +38,12 @@ export { AutonomousAgent, AutonomousAgentCalls, TaskCalls, Tasks, accepted, reje
|
|
|
38
38
|
export { Endpoint } from "./endpoint.js";
|
|
39
39
|
export { McpEndpoint, ToolTable, tools, MCP_ENDPOINTS_SINCE, } from "./mcp-endpoint.js";
|
|
40
40
|
export { ClientKeys, InMemoryClientKeys, Expiry, CLIENT_KEYS_SINCE, } from "./client-keys.js";
|
|
41
|
+
export { Erasures, ERASURES_SINCE } from "./erasures.js";
|
|
41
42
|
export { SocketClosed } from "./socket.js";
|
|
42
43
|
export { Acl, Callers, HttpProblem, get, post, put, patch, del, sse, sseEvent, sseEvents, socket } from "./routes.js";
|
|
43
44
|
export { Query, Headers } from "./context.js";
|
|
44
45
|
export { ComponentClient, Calls, TypedCalls, Invocation, AwaitingInvocation, AWAIT_SINCE, awaitPartJson, Views, Timers, Secrets, InMemorySecrets, noSecrets, WatchEnded, VIEW_STREAMS_SINCE } from "./client.js";
|
|
45
|
-
export { ObjectRules, StorageRefusals, InMemoryBuckets, DirectoryBuckets, S3Buckets, S3Bucket, noStorage, chooseStorage, declaredBuckets, processStorage, s3Settings, bucketNameOf, bucketVariableSuffix, localAddress, STORAGE_PATH, } from "./storage.js";
|
|
46
|
+
export { ObjectRules, StorageRefusals, InMemoryBuckets, DirectoryBuckets, S3Buckets, S3Bucket, noStorage, chooseStorage, declaredBuckets, processStorage, s3Settings, bucketNameOf, bucketVariableSuffix, localAddress, STORAGE_PATH, subjectPrefix, parseVersions, } from "./storage.js";
|
|
46
47
|
export { Credentials, SignedRequest, sign as signRequest, presign as presignUrl } from "./sigv4.js";
|
|
47
48
|
export { Services, ServiceClient, ScriptedServices, noServices, ServiceError, ServiceUnresolvable, ServiceIdentityMismatch, ServiceUnanswered, ServiceCallFailed, } from "./services.js";
|
|
48
49
|
export { Ankka, ServiceBuilder, Registry, RegistrationError, } from "./service.js";
|
|
@@ -53,3 +54,6 @@ export { PROTOCOL_VERSION, SDK_NAME } from "./spec.js";
|
|
|
53
54
|
export { StartFrom } from "./startFrom.js";
|
|
54
55
|
export { isTerminal, isUnknown } from "./standing.js";
|
|
55
56
|
export { materialiseEventSourced, materialiseKeyValue, materialiseWorkflowCommand, materialiseStep, } from "./materialise.js";
|
|
57
|
+
export { personal, subject, subjectProblem, isPersonal, Present, Erased, PersonalError, DATA_SUBJECT_RULE, } from "./personal.js";
|
|
58
|
+
export { Keyring, currentKeyring, keyringSettings, DEFAULT_KEYRING_SETTINGS } from "./keyring.js";
|
|
59
|
+
export { finalityToProto } from "./erasure.js";
|
package/dist/json.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
// schema-directed decode that converts, checks and names the path of anything it refuses.
|
|
8
8
|
import { Duration, Instant, LocalDate, LocalDateTime } from "./time.js";
|
|
9
9
|
import { done, resolve, describe } from "./schema.js";
|
|
10
|
+
import { associatedData, currentScope, Erased, ENVELOPE_VERSION, open, originOf, PersonalError, Present, readFrom, seal, subjectProblem } from "./personal.js";
|
|
11
|
+
import { requireKeyring } from "./keyring.js";
|
|
10
12
|
export class EncodingError extends Error {
|
|
11
13
|
constructor(message) {
|
|
12
14
|
super(message);
|
|
@@ -188,8 +190,69 @@ function write(schema, value, out, path) {
|
|
|
188
190
|
out.push(`{"type":${JSON.stringify(value)}}`);
|
|
189
191
|
return;
|
|
190
192
|
}
|
|
193
|
+
case "personal": {
|
|
194
|
+
if (!(value instanceof Present) && !(value instanceof Erased)) {
|
|
195
|
+
throw new EncodingError(`${at(path)}expected a personal field (personal(subject, value)), got ${kindOf(value)}`);
|
|
196
|
+
}
|
|
197
|
+
const problem = subjectProblem(value.subject);
|
|
198
|
+
if (problem !== undefined)
|
|
199
|
+
throw new EncodingError(`${at(path)}${problem}`);
|
|
200
|
+
const scope = currentScope();
|
|
201
|
+
if (scope === undefined) {
|
|
202
|
+
// The edge: the value with its subject beside it.
|
|
203
|
+
out.push(`{"subject":${JSON.stringify(value.subject)}`);
|
|
204
|
+
if (value instanceof Present) {
|
|
205
|
+
out.push(`,"value":`);
|
|
206
|
+
write(r.inner, value.value, out, path ? `${path}.value` : "value");
|
|
207
|
+
}
|
|
208
|
+
out.push("}");
|
|
209
|
+
}
|
|
210
|
+
else
|
|
211
|
+
writeEnvelope(r.inner, value, scope, out, path);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function writeEnvelope(inner, value, scope, out, path) {
|
|
217
|
+
let project;
|
|
218
|
+
let c;
|
|
219
|
+
const origin = originOf(value);
|
|
220
|
+
if (origin !== undefined && origin !== requireKeyring().project) {
|
|
221
|
+
// Read from another project's envelope: kept under that project's key (FR-027), so an erasure
|
|
222
|
+
// there reaches what this service keeps of it, with no notice of its own. A key it can no longer
|
|
223
|
+
// have writes the field erased, as it would now be read.
|
|
224
|
+
project = origin;
|
|
225
|
+
if (value instanceof Present) {
|
|
226
|
+
const answer = requireKeyring().keyForRead(origin, value.subject);
|
|
227
|
+
if (answer.kind === "key") {
|
|
228
|
+
const plaintext = [];
|
|
229
|
+
write(inner, value.value, plaintext, path);
|
|
230
|
+
c = base64Encode(seal(answer.key, utf8.encode(plaintext.join("")), associatedData(value.subject, origin, scope.manifest), scope.nonce));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
else if (value instanceof Present) {
|
|
235
|
+
const { project: own, key } = requireKeyring().keyForWrite(value.subject);
|
|
236
|
+
project = own;
|
|
237
|
+
const plaintext = [];
|
|
238
|
+
write(inner, value.value, plaintext, path);
|
|
239
|
+
const sealed = seal(key, utf8.encode(plaintext.join("")), associatedData(value.subject, project, scope.manifest), scope.nonce);
|
|
240
|
+
c = base64Encode(sealed);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
// Written again after a redaction: the subject alone, under the service's own project.
|
|
244
|
+
const keyring = requireKeyring();
|
|
245
|
+
if (keyring.project === undefined)
|
|
246
|
+
throw PersonalError.unavailable("this process does not know its project yet: no key answer from the sidecar has named it");
|
|
247
|
+
project = keyring.project;
|
|
191
248
|
}
|
|
249
|
+
out.push(`{"$personal":${ENVELOPE_VERSION},"project":${JSON.stringify(project)},"subject":${JSON.stringify(value.subject)}`);
|
|
250
|
+
if (c !== undefined)
|
|
251
|
+
out.push(`,"c":${JSON.stringify(c)}`);
|
|
252
|
+
out.push("}");
|
|
192
253
|
}
|
|
254
|
+
const utf8 = new TextEncoder();
|
|
255
|
+
const utf8Strict = new TextDecoder("utf-8", { fatal: true });
|
|
193
256
|
function writeFields(fields, value, out, path, continuing) {
|
|
194
257
|
let first = !continuing;
|
|
195
258
|
for (const [name, field] of Object.entries(fields)) {
|
|
@@ -314,7 +377,54 @@ function decode(schema, raw, path) {
|
|
|
314
377
|
}
|
|
315
378
|
return name;
|
|
316
379
|
}
|
|
380
|
+
case "personal":
|
|
381
|
+
return currentScope() === undefined ? decodeEdge(r.inner, raw, path) : decodeEnvelope(r.inner, raw, currentScope(), path);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function decodeEdge(inner, raw, path) {
|
|
385
|
+
if (!isPlainObject(raw))
|
|
386
|
+
throw new DecodingError(`expected a personal field {"subject", "value"}, got ${kindOf(raw)}`, path);
|
|
387
|
+
const about = raw["subject"];
|
|
388
|
+
if (about === undefined)
|
|
389
|
+
throw new DecodingError("a personal field names its data subject", path);
|
|
390
|
+
const problem = subjectProblem(about);
|
|
391
|
+
if (problem !== undefined)
|
|
392
|
+
throw new DecodingError(problem, path ? `${path}.subject` : "subject");
|
|
393
|
+
if (!("value" in raw))
|
|
394
|
+
return new Erased(about);
|
|
395
|
+
return new Present(about, decode(inner, raw["value"], path ? `${path}.value` : "value"));
|
|
396
|
+
}
|
|
397
|
+
function decodeEnvelope(inner, raw, scope, path) {
|
|
398
|
+
if (!isPlainObject(raw))
|
|
399
|
+
throw new DecodingError(`expected a personal envelope, got ${kindOf(raw)}`, path);
|
|
400
|
+
const { project, subject: about, c } = raw;
|
|
401
|
+
if (typeof about !== "string" || typeof project !== "string")
|
|
402
|
+
throw new DecodingError("a personal envelope names its project and its data subject", path);
|
|
403
|
+
const problem = subjectProblem(about);
|
|
404
|
+
if (problem !== undefined)
|
|
405
|
+
throw new DecodingError(problem, path ? `${path}.subject` : "subject");
|
|
406
|
+
const s = about;
|
|
407
|
+
if (raw["$personal"] !== ENVELOPE_VERSION) {
|
|
408
|
+
throw PersonalError.corrupt(about, `its version is ${String(raw["$personal"])}, and this reader knows ${ENVELOPE_VERSION}`);
|
|
409
|
+
}
|
|
410
|
+
if (c === undefined)
|
|
411
|
+
return readFrom(new Erased(s), project);
|
|
412
|
+
const answer = requireKeyring().keyForRead(project, about);
|
|
413
|
+
if (answer.kind === "erased")
|
|
414
|
+
return readFrom(new Erased(s), project);
|
|
415
|
+
if (typeof c !== "string" || !/^[A-Za-z0-9+/]*={0,2}$/.test(c))
|
|
416
|
+
throw PersonalError.corrupt(about, "it is not base64");
|
|
417
|
+
const plaintext = open(answer.key, base64Decode(c), associatedData(about, project, scope.manifest));
|
|
418
|
+
if (plaintext === undefined)
|
|
419
|
+
throw PersonalError.corrupt(about, "it failed authentication");
|
|
420
|
+
let parsed;
|
|
421
|
+
try {
|
|
422
|
+
parsed = JSON.parse(utf8Strict.decode(plaintext), reviver);
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
throw PersonalError.corrupt(about, "its plaintext is not JSON");
|
|
317
426
|
}
|
|
427
|
+
return readFrom(new Present(s, decode(inner, parsed, path)), project);
|
|
318
428
|
}
|
|
319
429
|
function decodeFields(fields, raw, path) {
|
|
320
430
|
const out = {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/** What the keyring answered for one data subject. */
|
|
2
|
+
export type KeyAnswer = {
|
|
3
|
+
readonly kind: "key";
|
|
4
|
+
readonly key: Uint8Array;
|
|
5
|
+
} | {
|
|
6
|
+
readonly kind: "erased";
|
|
7
|
+
};
|
|
8
|
+
/** A key answer and the project it is for: the service's own when the request left `project` empty. */
|
|
9
|
+
export interface KeyReply {
|
|
10
|
+
readonly project: string;
|
|
11
|
+
readonly answer: KeyAnswer;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* How a key is fetched: `project` empty is the service's own, and `create` makes one for a write.
|
|
15
|
+
* `home`, when given, is the installation whose keyring keeps the key: a replicated entity's home
|
|
16
|
+
* elsewhere (FR-032), asked through the sidecar's relay and never with `create`.
|
|
17
|
+
*/
|
|
18
|
+
export type KeyFetch = (project: string, subject: string, create: boolean, home?: string) => Promise<KeyReply>;
|
|
19
|
+
/**
|
|
20
|
+
* How a process holds the keys it fetched: the runtime's `ankka.keyring` settings, by the same names
|
|
21
|
+
* and defaults, in milliseconds. `keyringSettings` refuses one that breaks a rule, naming it.
|
|
22
|
+
*/
|
|
23
|
+
export interface KeyringSettings {
|
|
24
|
+
/** `ankka.keyring.cache.max-keys`: how many keys are held at once; the least recently used goes first. */
|
|
25
|
+
readonly maxKeys: number;
|
|
26
|
+
/** `ankka.keyring.cache.ttl`: how long a key of the own project is held before it is asked for again. */
|
|
27
|
+
readonly ttlMillis: number;
|
|
28
|
+
/** `ankka.keyring.cache.foreign-ttl`: how long a key of another project, or a read's erased answer, is held. */
|
|
29
|
+
readonly foreignTtlMillis: number;
|
|
30
|
+
/** `ankka.keyring.cache.outage-bound`: how long after its fetch a held own key serves while the keyring cannot be reached. */
|
|
31
|
+
readonly outageBoundMillis: number;
|
|
32
|
+
/** `ankka.keyring.notice-bound`: the bound on an erasure reaching a read, which the outage bound may not exceed. */
|
|
33
|
+
readonly noticeBoundMillis: number;
|
|
34
|
+
}
|
|
35
|
+
export declare const DEFAULT_KEYRING_SETTINGS: KeyringSettings;
|
|
36
|
+
/** The defaults with `given` over them; throws a `RangeError` naming the setting a value breaks. */
|
|
37
|
+
export declare function keyringSettings(given?: Partial<KeyringSettings>): KeyringSettings;
|
|
38
|
+
export interface KeyringOptions {
|
|
39
|
+
readonly fetch: KeyFetch;
|
|
40
|
+
readonly settings?: Partial<KeyringSettings>;
|
|
41
|
+
/** Milliseconds now; a test moves it. */
|
|
42
|
+
readonly clock?: () => number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The process's key cache, under the runtime's rules: at most `maxKeys`, the least recently used going
|
|
46
|
+
* first; a key of the own project fresh for `ttl`, and still served while the keyring cannot be reached
|
|
47
|
+
* until `outageBound` after it was fetched; a key of another project, which no notice of that project
|
|
48
|
+
* reaches, and a read's erased answer, for `foreignTtl` and no longer. A write's erased answer is a
|
|
49
|
+
* destroyed key, which never changes. An answer whose fetch was in flight when a notice dropped its
|
|
50
|
+
* subject may be the destroyed key: it is never cached, and the key is asked for again.
|
|
51
|
+
*/
|
|
52
|
+
export declare class Keyring {
|
|
53
|
+
#private;
|
|
54
|
+
constructor(options: KeyringOptions);
|
|
55
|
+
/** The service's own project, as the first key answer for it named it; `undefined` until then. */
|
|
56
|
+
get project(): string | undefined;
|
|
57
|
+
get settings(): KeyringSettings;
|
|
58
|
+
/** How many answers are held. */
|
|
59
|
+
get size(): number;
|
|
60
|
+
/**
|
|
61
|
+
* `work`, run with the keys of `home`'s keyring: a replicated entity's home as its records name it
|
|
62
|
+
* (FR-032), "" for this installation's. `work` is a synchronous codec call, so nothing else runs
|
|
63
|
+
* while the home is set.
|
|
64
|
+
*/
|
|
65
|
+
within<T>(home: string, work: () => T): T;
|
|
66
|
+
/**
|
|
67
|
+
* Fetches, unless held fresh, the key of every subject the envelopes in `json` name, from `home`'s
|
|
68
|
+
* keyring ("" for this installation's). Never throws.
|
|
69
|
+
*/
|
|
70
|
+
prefetchJson(json: Uint8Array | string, home?: string): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Fetches, unless held fresh, the key of every present personal field in `value`, for a write. An
|
|
73
|
+
* erased field needs no key, but its envelope names the own project: until an answer has named it, one
|
|
74
|
+
* is asked for, without `create`. A field read from another project's envelope is sealed again under
|
|
75
|
+
* that project's key (FR-027): its key is fetched, never made. `home` is whose keyring keeps the
|
|
76
|
+
* keys, "" for this installation's; another home's keys are never made. Never throws.
|
|
77
|
+
*/
|
|
78
|
+
prefetchValue(value: unknown, home?: string): Promise<void>;
|
|
79
|
+
/** The key a write seals `subject`'s field with. Throws `Erased` or `Unavailable`; never fetches. */
|
|
80
|
+
keyForWrite(subject: string): {
|
|
81
|
+
project: string;
|
|
82
|
+
key: Uint8Array;
|
|
83
|
+
};
|
|
84
|
+
/** The key a read opens `subject`'s field of `project` with, or `erased`. Throws `Unavailable`; never fetches. */
|
|
85
|
+
keyForRead(project: string, subject: string): KeyAnswer;
|
|
86
|
+
/**
|
|
87
|
+
* Forgets every key: what the process does when its notice stream ends or opens again, since the
|
|
88
|
+
* sidecar counts a closed stream as acked and hands a notice sent while none was held never again.
|
|
89
|
+
*/
|
|
90
|
+
dropAll(): void;
|
|
91
|
+
/** Forgets `subject`'s key, from whichever home it was fetched: what a notice from the sidecar does. */
|
|
92
|
+
drop(project: string, subject: string): void;
|
|
93
|
+
/** Whether a key, or an erased answer, is held for `subject` and may be used. */
|
|
94
|
+
holds(project: string, subject: string, home?: string): boolean;
|
|
95
|
+
}
|
|
96
|
+
/** @internal Installs the keyring every codec in this process reads; `undefined` removes it. */
|
|
97
|
+
export declare function installKeyring(keyring: Keyring | undefined): void;
|
|
98
|
+
/** The keyring installed in this process, if any. */
|
|
99
|
+
export declare function currentKeyring(): Keyring | undefined;
|
|
100
|
+
/** The installed keyring; `Unavailable` when there is none. */
|
|
101
|
+
export declare function requireKeyring(): Keyring;
|
|
102
|
+
/** Prefetches for decoding `json` from `home`'s keyring, when a keyring is installed. Never throws. */
|
|
103
|
+
export declare function prefetchForDecode(json: Uint8Array | string | undefined, home?: string): Promise<void>;
|
|
104
|
+
/** Prefetches for encoding `value` with `home`'s keys, when a keyring is installed. Never throws. */
|
|
105
|
+
export declare function prefetchForEncode(value: unknown, home?: string): Promise<void>;
|
|
106
|
+
/** `work` with the keys of `home`'s keyring ("" for this installation's), when a keyring is installed. */
|
|
107
|
+
export declare function withKeysAt<T>(home: string, work: () => T): T;
|
|
108
|
+
/** A notice as the sidecar relays it. */
|
|
109
|
+
export interface SubjectNotice {
|
|
110
|
+
readonly seq: bigint;
|
|
111
|
+
readonly project: string;
|
|
112
|
+
readonly subject: string;
|
|
113
|
+
readonly rerun: boolean;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Holds `open()`'s notice stream for as long as `signal` is not aborted: each notice drops the subject's
|
|
117
|
+
* key and is then acked. A stream that ends or fails is opened again after `retryMillis`.
|
|
118
|
+
*/
|
|
119
|
+
export declare function followNotices(keyring: Keyring, open: (signal: AbortSignal) => AsyncIterable<SubjectNotice>, ack: (seq: bigint) => Promise<void>, signal: AbortSignal, log?: (message: string) => void, retryMillis?: number): Promise<void>;
|