@keri-ts/hosting 0.0.0-stage → 2.0.1

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.
Files changed (67) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +22 -2
  3. package/esm/hosting/npm/src/_dnt.shims.js +61 -0
  4. package/esm/hosting/npm/src/mod.js +7 -0
  5. package/esm/hosting/npm/src/src/host/http-server.js +310 -0
  6. package/esm/hosting/npm/src/src/host/kernel.js +83 -0
  7. package/esm/hosting/npm/src/src/http/app.js +33 -0
  8. package/esm/hosting/npm/src/src/http/error-mapping.js +19 -0
  9. package/esm/hosting/npm/src/src/http/policy.js +63 -0
  10. package/esm/hosting/npm/src/src/http/protocol/artifact-files.js +164 -0
  11. package/esm/hosting/npm/src/src/http/protocol/context.js +48 -0
  12. package/esm/hosting/npm/src/src/http/protocol/endpoints/did-resolution.js +90 -0
  13. package/esm/hosting/npm/src/src/http/protocol/endpoints/dws-artifacts.js +112 -0
  14. package/esm/hosting/npm/src/src/http/protocol/endpoints/generic-cesr-ingress.js +272 -0
  15. package/esm/hosting/npm/src/src/http/protocol/endpoints/health.js +9 -0
  16. package/esm/hosting/npm/src/src/http/protocol/endpoints/mailbox-admin.js +136 -0
  17. package/esm/hosting/npm/src/src/http/protocol/endpoints/oobi.js +171 -0
  18. package/esm/hosting/npm/src/src/http/protocol/endpoints/witness-http.js +98 -0
  19. package/esm/hosting/npm/src/src/http/protocol/responses.js +21 -0
  20. package/esm/hosting/npm/src/src/http/protocol/runtime-bridge.js +16 -0
  21. package/esm/hosting/npm/src/src/http/protocol/types.js +1 -0
  22. package/esm/hosting/npm/src/src/http/protocol-handler.js +68 -0
  23. package/esm/hosting/npm/src/src/http/source.js +118 -0
  24. package/esm/package.json +3 -0
  25. package/package.json +43 -4
  26. package/types/_dnt.shims.d.ts +6 -0
  27. package/types/_dnt.shims.d.ts.map +1 -0
  28. package/types/mod.d.ts +9 -0
  29. package/types/mod.d.ts.map +1 -0
  30. package/types/src/host/http-server.d.ts +46 -0
  31. package/types/src/host/http-server.d.ts.map +1 -0
  32. package/types/src/host/kernel.d.ts +37 -0
  33. package/types/src/host/kernel.d.ts.map +1 -0
  34. package/types/src/http/app.d.ts +22 -0
  35. package/types/src/http/app.d.ts.map +1 -0
  36. package/types/src/http/error-mapping.d.ts +7 -0
  37. package/types/src/http/error-mapping.d.ts.map +1 -0
  38. package/types/src/http/policy.d.ts +27 -0
  39. package/types/src/http/policy.d.ts.map +1 -0
  40. package/types/src/http/protocol/artifact-files.d.ts +11 -0
  41. package/types/src/http/protocol/artifact-files.d.ts.map +1 -0
  42. package/types/src/http/protocol/context.d.ts +14 -0
  43. package/types/src/http/protocol/context.d.ts.map +1 -0
  44. package/types/src/http/protocol/endpoints/did-resolution.d.ts +12 -0
  45. package/types/src/http/protocol/endpoints/did-resolution.d.ts.map +1 -0
  46. package/types/src/http/protocol/endpoints/dws-artifacts.d.ts +9 -0
  47. package/types/src/http/protocol/endpoints/dws-artifacts.d.ts.map +1 -0
  48. package/types/src/http/protocol/endpoints/generic-cesr-ingress.d.ts +21 -0
  49. package/types/src/http/protocol/endpoints/generic-cesr-ingress.d.ts.map +1 -0
  50. package/types/src/http/protocol/endpoints/health.d.ts +6 -0
  51. package/types/src/http/protocol/endpoints/health.d.ts.map +1 -0
  52. package/types/src/http/protocol/endpoints/mailbox-admin.d.ts +26 -0
  53. package/types/src/http/protocol/endpoints/mailbox-admin.d.ts.map +1 -0
  54. package/types/src/http/protocol/endpoints/oobi.d.ts +31 -0
  55. package/types/src/http/protocol/endpoints/oobi.d.ts.map +1 -0
  56. package/types/src/http/protocol/endpoints/witness-http.d.ts +13 -0
  57. package/types/src/http/protocol/endpoints/witness-http.d.ts.map +1 -0
  58. package/types/src/http/protocol/responses.d.ts +7 -0
  59. package/types/src/http/protocol/responses.d.ts.map +1 -0
  60. package/types/src/http/protocol/runtime-bridge.d.ts +13 -0
  61. package/types/src/http/protocol/runtime-bridge.d.ts.map +1 -0
  62. package/types/src/http/protocol/types.d.ts +78 -0
  63. package/types/src/http/protocol/types.d.ts.map +1 -0
  64. package/types/src/http/protocol-handler.d.ts +19 -0
  65. package/types/src/http/protocol-handler.d.ts.map +1 -0
  66. package/types/src/http/source.d.ts +25 -0
  67. package/types/src/http/source.d.ts.map +1 -0
@@ -0,0 +1,83 @@
1
+ import { spawn } from "effection";
2
+ import { systemClock } from "keri-ts/runtime";
3
+ import { FetchHttpClient } from "keri-ts/runtime";
4
+ import { consoleLogger, createAgentRuntime, runAgentRuntime, } from "keri-ts/runtime";
5
+ import { startServer } from "./http-server.js";
6
+ /**
7
+ * Compose and run the long-lived host over one borrowed Habery.
8
+ * Select HTTP/clock/polling implementations at this root, acquire runtime/listener
9
+ * resources in scope, and join dependent workers before owned store teardown.
10
+ *
11
+ * @param hby Borrowed local identities and accepted-state stores.
12
+ * @param spec Explicit hosted identities, listener settings, protocol handlers and host policy.
13
+ */
14
+ export function* runHostKernel(hby, spec) {
15
+ const runtime = yield* createAgentRuntime(hby, new FetchHttpClient(), systemClock, {
16
+ enableMailboxStore: spec.enableMailboxStore,
17
+ kram: spec.kram,
18
+ });
19
+ const hostedPrefixes = spec.hostedPrefixes ?? [spec.serviceHab.pre];
20
+ const protocolPolicy = {
21
+ ...spec.protocolPolicy,
22
+ serviceHab: spec.serviceHab,
23
+ hostedPrefixes,
24
+ };
25
+ /** Runtime worker starts only after HTTP listener acquisition; absent if startup fails earlier. */
26
+ let runtimeTask;
27
+ const companionTasks = [];
28
+ let closed = false;
29
+ /** Join companions, then runtime workers and owned stores; retain the first failure while attempting every close. */
30
+ const closeKernel = function* () {
31
+ if (closed) {
32
+ return;
33
+ }
34
+ let failed = false;
35
+ let failure;
36
+ for (const task of companionTasks.slice().reverse()) {
37
+ try {
38
+ yield* task.halt();
39
+ }
40
+ catch (error) {
41
+ failed = true;
42
+ failure ??= error;
43
+ }
44
+ }
45
+ try {
46
+ if (runtimeTask)
47
+ yield* runtimeTask.halt();
48
+ }
49
+ catch (error) {
50
+ failed = true;
51
+ failure ??= error;
52
+ }
53
+ try {
54
+ yield* runtime.close();
55
+ }
56
+ catch (error) {
57
+ failed = true;
58
+ failure ??= error;
59
+ }
60
+ closed = true;
61
+ if (failed) {
62
+ throw failure;
63
+ }
64
+ };
65
+ try {
66
+ for (const companion of spec.companionHosts ?? []) {
67
+ companionTasks.push(yield* spawn(function* () {
68
+ yield* companion(runtime);
69
+ }));
70
+ }
71
+ // Acquire the listener before consumers can poll a locally hosted provider.
72
+ const serverTask = yield* spawn(() => startServer(spec.http.port, consoleLogger, runtime, {
73
+ hostname: spec.http.hostname,
74
+ onListen: spec.http.onListen,
75
+ ...protocolPolicy,
76
+ }, closeKernel));
77
+ runtimeTask = yield* spawn(() => runAgentRuntime(runtime, { hab: spec.serviceHab }));
78
+ yield* serverTask;
79
+ }
80
+ finally {
81
+ yield* closeKernel();
82
+ }
83
+ }
@@ -0,0 +1,33 @@
1
+ import { Hono } from "hono";
2
+ import { installTufaHttpErrorHandling } from "./error-mapping.js";
3
+ import { installTufaHttpPolicy, resolveTufaHttpAppPolicy } from "./policy.js";
4
+ import { createProtocolHandler } from "./protocol-handler.js";
5
+ /**
6
+ * Build the Stage 4 Hono shell for the Tufa HTTP edge.
7
+ *
8
+ * Stage 4 keeps route semantics inside the protocol handler while moving
9
+ * cross-cutting HTTP policy into explicit hosting-owned middleware and error
10
+ * mapping.
11
+ */
12
+ export function createTufaApp(options = {}) {
13
+ const app = new Hono();
14
+ const policy = resolveTufaHttpAppPolicy(options.app);
15
+ let handler = options.protocolHandler;
16
+ let ownsHandler = false;
17
+ if (!handler) {
18
+ handler = createProtocolHandler(options.runtime, options.protocolPolicy ?? {});
19
+ ownsHandler = true;
20
+ }
21
+ installTufaHttpPolicy(app, policy);
22
+ installTufaHttpErrorHandling(app, policy);
23
+ app.all("*", async (context) => {
24
+ return await handler(context.req.raw);
25
+ });
26
+ return Object.assign(app, {
27
+ /** Join the default protocol handler's file responses; injected handlers stay caller-owned. */
28
+ async close() {
29
+ if (ownsHandler)
30
+ await handler.close?.();
31
+ },
32
+ });
33
+ }
@@ -0,0 +1,19 @@
1
+ import { ValidationError } from "keri-ts/runtime";
2
+ import { applyTufaCorsHeaders } from "./policy.js";
3
+ import { textResponse } from "./protocol/responses.js";
4
+ /** Map one unhandled edge error into the Stage 4 HTTP response contract. */
5
+ export function mapTufaHttpError(error, policy) {
6
+ if (error instanceof ValidationError) {
7
+ return textResponse(error.message, 400);
8
+ }
9
+ policy.logger.error("Unhandled HTTP app error", error);
10
+ return textResponse("Internal Server Error", 500);
11
+ }
12
+ /** Install the centralized Stage 4 app-level error mapping. */
13
+ export function installTufaHttpErrorHandling(app, policy) {
14
+ app.onError((error, _context) => {
15
+ const response = mapTufaHttpError(error, policy);
16
+ applyTufaCorsHeaders(response.headers, policy.cors);
17
+ return response;
18
+ });
19
+ }
@@ -0,0 +1,63 @@
1
+ import { consoleLogger } from "keri-ts/runtime";
2
+ import { normalizeProtocolPath } from "./protocol/context.js";
3
+ export const DEFAULT_TUFA_CORS_POLICY = Object.freeze({
4
+ allowOrigin: "*",
5
+ allowMethods: Object.freeze(["GET", "POST", "PUT", "OPTIONS"]),
6
+ allowHeaders: Object.freeze([
7
+ "Content-Type",
8
+ "CESR-ATTACHMENT",
9
+ "CESR-DESTINATION",
10
+ "Oobi-Aid",
11
+ ]),
12
+ exposeHeaders: Object.freeze([
13
+ "Content-Type",
14
+ "KERI-AID",
15
+ "Oobi-Aid",
16
+ ]),
17
+ });
18
+ /** Resolve one app policy with Stage 4 defaults. */
19
+ export function resolveTufaHttpAppPolicy(options = {}) {
20
+ return {
21
+ logger: options.logger ?? consoleLogger,
22
+ cors: {
23
+ allowOrigin: options.cors?.allowOrigin
24
+ ?? DEFAULT_TUFA_CORS_POLICY.allowOrigin,
25
+ allowMethods: options.cors?.allowMethods
26
+ ?? DEFAULT_TUFA_CORS_POLICY.allowMethods,
27
+ allowHeaders: options.cors?.allowHeaders
28
+ ?? DEFAULT_TUFA_CORS_POLICY.allowHeaders,
29
+ exposeHeaders: options.cors?.exposeHeaders
30
+ ?? DEFAULT_TUFA_CORS_POLICY.exposeHeaders,
31
+ },
32
+ };
33
+ }
34
+ /** Install the Stage 4 middleware envelope onto one Hono app. */
35
+ export function installTufaHttpPolicy(app, policy) {
36
+ app.use("*", createTufaHttpPolicyMiddleware(policy));
37
+ }
38
+ function createTufaHttpPolicyMiddleware(policy) {
39
+ return async (context, next) => {
40
+ const startedAt = performance.now();
41
+ if (context.req.method === "OPTIONS") {
42
+ const response = new Response(null, { status: 204 });
43
+ applyTufaCorsHeaders(response.headers, policy.cors);
44
+ logRequest(policy.logger, context.req.url, context.req.method, 204, startedAt);
45
+ return response;
46
+ }
47
+ await next();
48
+ applyTufaCorsHeaders(context.res.headers, policy.cors);
49
+ logRequest(policy.logger, context.req.url, context.req.method, context.res.status, startedAt);
50
+ };
51
+ }
52
+ /** Apply the resolved Stage 4 CORS policy to one response header set. */
53
+ export function applyTufaCorsHeaders(headers, policy) {
54
+ headers.set("Access-Control-Allow-Origin", policy.allowOrigin);
55
+ headers.set("Access-Control-Allow-Methods", policy.allowMethods.join(", "));
56
+ headers.set("Access-Control-Allow-Headers", policy.allowHeaders.join(", "));
57
+ headers.set("Access-Control-Expose-Headers", policy.exposeHeaders.join(", "));
58
+ }
59
+ function logRequest(logger, rawUrl, method, status, startedAt) {
60
+ const pathname = normalizeProtocolPath(new URL(rawUrl).pathname);
61
+ const durationMs = Math.max(0, performance.now() - startedAt).toFixed(1);
62
+ logger.info(`${method} ${pathname} -> ${status} ${durationMs}ms`);
63
+ }
@@ -0,0 +1,164 @@
1
+ /** Demand-driven static artifact responses; the host owns open files through joined response cleanup. */
2
+ import { constants } from "node:fs";
3
+ import { open } from "node:fs/promises";
4
+ const CHUNK = 64 * 1024;
5
+ /** Own one host's open file responses, including bodies that have not been consumed. */
6
+ export class ArtifactFiles {
7
+ constructor() {
8
+ Object.defineProperty(this, "owners", {
9
+ enumerable: true,
10
+ configurable: true,
11
+ writable: true,
12
+ value: new Set()
13
+ });
14
+ Object.defineProperty(this, "stopping", {
15
+ enumerable: true,
16
+ configurable: true,
17
+ writable: true,
18
+ value: false
19
+ });
20
+ Object.defineProperty(this, "closing", {
21
+ enumerable: true,
22
+ configurable: true,
23
+ writable: true,
24
+ value: void 0
25
+ });
26
+ }
27
+ /** Open the regular file and capture its extent without reading data. */
28
+ async response(path, contentType, signal) {
29
+ signal.throwIfAborted();
30
+ if (this.stopping)
31
+ throw new DOMException("Artifact host closed", "AbortError");
32
+ let file, reading;
33
+ let controller;
34
+ let ended = false, closing;
35
+ // Nonblocking open lets the regular-file check reject pipes without hanging acquisition/shutdown.
36
+ const opening = Promise.resolve().then(() => open(path, constants.O_RDONLY | constants.O_NONBLOCK));
37
+ /** Stop new reads and join acquisition, the current read and file close. */
38
+ const close = (reason) => {
39
+ if (closing)
40
+ return closing;
41
+ ended = true;
42
+ closing = (async () => {
43
+ const failures = [];
44
+ try {
45
+ try {
46
+ if (reason !== undefined)
47
+ controller?.error(reason);
48
+ }
49
+ catch (error) {
50
+ failures.push(error);
51
+ }
52
+ const acquired = await opening.catch(() => undefined);
53
+ try {
54
+ await reading;
55
+ }
56
+ catch (error) {
57
+ failures.push(error);
58
+ }
59
+ try {
60
+ await acquired?.close();
61
+ }
62
+ catch (error) {
63
+ if (!failures.includes(error))
64
+ failures.push(error);
65
+ }
66
+ }
67
+ finally {
68
+ this.owners.delete(close);
69
+ }
70
+ if (failures.length === 1)
71
+ throw failures[0];
72
+ if (failures.length)
73
+ throw new AggregateError(failures, "Artifact read and cleanup failed");
74
+ })();
75
+ return closing;
76
+ };
77
+ this.owners.add(close);
78
+ try {
79
+ file = await opening;
80
+ const stat = await file.stat();
81
+ signal.throwIfAborted(); // The request signal owns acquisition, never the returned response lifetime.
82
+ if (ended)
83
+ throw new DOMException("Artifact host closed", "AbortError");
84
+ if (!stat.isFile()) {
85
+ await close();
86
+ return new Response("Not Found", { status: 404 });
87
+ }
88
+ if (!Number.isSafeInteger(stat.size) || stat.size < 0)
89
+ throw new RangeError("Invalid artifact extent");
90
+ if (stat.size === 0) {
91
+ await close();
92
+ return new Response(null, { headers: { "Content-Type": contentType, "Content-Length": "0" } });
93
+ }
94
+ let remaining = stat.size;
95
+ const body = new ReadableStream({
96
+ start(value) {
97
+ controller = value;
98
+ },
99
+ pull: async (output) => {
100
+ if (ended)
101
+ return;
102
+ try {
103
+ if (remaining === 0) {
104
+ await close();
105
+ output.close();
106
+ return;
107
+ }
108
+ const chunk = new Uint8Array(Math.min(CHUNK, remaining));
109
+ // The positional overload requires an explicit extent for this typed buffer.
110
+ reading = file.read(chunk, 0, chunk.byteLength, null);
111
+ const { bytesRead } = await reading;
112
+ reading = undefined;
113
+ if (ended)
114
+ return;
115
+ if (bytesRead === 0)
116
+ throw new Error("Artifact truncated during response");
117
+ remaining -= bytesRead;
118
+ output.enqueue(chunk.subarray(0, bytesRead));
119
+ }
120
+ catch (error) {
121
+ reading = undefined; // This read has settled; close must still join any concurrently started cleanup.
122
+ let failure = error;
123
+ try {
124
+ await close();
125
+ }
126
+ catch (cleanup) {
127
+ const failures = cleanup instanceof AggregateError ? cleanup.errors : [cleanup];
128
+ failure = failures.includes(error)
129
+ ? cleanup
130
+ : new AggregateError([error, ...failures], "Artifact read and cleanup failed");
131
+ }
132
+ output.error(failure);
133
+ }
134
+ },
135
+ cancel: () => close(),
136
+ }, { highWaterMark: 0 });
137
+ return new Response(body, {
138
+ headers: { "Content-Type": contentType, "Content-Length": String(stat.size) },
139
+ });
140
+ }
141
+ catch (error) {
142
+ try {
143
+ await close();
144
+ }
145
+ catch (cleanup) {
146
+ throw new AggregateError([error, cleanup], "Artifact acquisition and cleanup failed");
147
+ }
148
+ throw error;
149
+ }
150
+ }
151
+ /** Close every file, including pending acquisitions, before the HTTP host finishes shutdown. */
152
+ close() {
153
+ this.stopping = true;
154
+ return this.closing ??= (async () => {
155
+ const reason = new DOMException("Artifact host closed", "AbortError");
156
+ const results = await Promise.allSettled([...this.owners].map(async (close) => await close(reason)));
157
+ const failures = results.filter((result) => result.status === "rejected").map((result) => result.reason);
158
+ if (failures.length === 1)
159
+ throw failures[0];
160
+ if (failures.length)
161
+ throw new AggregateError(failures, "Artifact host cleanup failed");
162
+ })();
163
+ }
164
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Request-context construction for Tufa protocol routing.
3
+ *
4
+ * The router is path-first: every endpoint classifier receives the same
5
+ * normalized path plus hosted-prefix projections, so mailbox admin, OOBI, and
6
+ * generic CESR ingress cannot disagree about which local AID a URL addresses.
7
+ */
8
+ import { resolveHostedEndpointPath } from "keri-ts/runtime";
9
+ import { parseOobiRouteRequest } from "./endpoints/oobi.js";
10
+ /** Snapshot one request into the path-first routing context. */
11
+ export function buildProtocolRequestContext(req, runtime, policy = {}) {
12
+ const url = new URL(req.url);
13
+ const pathname = normalizeProtocolPath(url.pathname);
14
+ // Resolve the same normalized path against the three hosted path shapes used
15
+ // by the HTTP edge: plain hosted endpoints, mailbox admin endpoints, and
16
+ // generic root-relative CESR ingress.
17
+ const hosted = runtime
18
+ ? resolveHostedEndpointPath(runtime.hby, pathname, "", policy.hostedPrefixes)
19
+ : null;
20
+ const mailboxAdmin = runtime
21
+ ? resolveHostedEndpointPath(runtime.hby, pathname, "/mailboxes", policy.hostedPrefixes)
22
+ : null;
23
+ const genericIngress = runtime
24
+ ? resolveHostedEndpointPath(runtime.hby, pathname, "/", policy.hostedPrefixes)
25
+ : null;
26
+ const oobiPath = hosted?.relativePath ?? pathname;
27
+ return {
28
+ req,
29
+ url,
30
+ pathname,
31
+ method: req.method,
32
+ runtime,
33
+ policy,
34
+ hosted,
35
+ mailboxAdmin,
36
+ genericIngress,
37
+ oobi: parseOobiRouteRequest(oobiPath),
38
+ };
39
+ }
40
+ /** Normalize incoming request paths into the comparison form used by routing. */
41
+ export function normalizeProtocolPath(pathname) {
42
+ const trimmed = pathname.trim();
43
+ if (trimmed.length === 0 || trimmed === "/") {
44
+ return "/";
45
+ }
46
+ const normalized = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
47
+ return normalized.replace(/\/+$/, "") || "/";
48
+ }
@@ -0,0 +1,90 @@
1
+ /** Universal Resolver compatible DID route. */
2
+ import { run } from "effection";
3
+ import { didResolutionError, parseDidKeriIdentifier, resolveDidKeri, resolveDidWebs, runtimeOobiConverged, } from "keri-ts/runtime";
4
+ const IDENTIFIERS_PREFIX = "/1.0/identifiers/";
5
+ /** Classify `/1.0/identifiers/{did}` requests. */
6
+ export function classifyDidResolutionRoute(context) {
7
+ if (context.method !== "GET" || !context.pathname.startsWith(IDENTIFIERS_PREFIX)) {
8
+ return null;
9
+ }
10
+ const encoded = context.pathname.slice(IDENTIFIERS_PREFIX.length);
11
+ if (encoded.length === 0) {
12
+ return null;
13
+ }
14
+ return { kind: "didResolution", did: decodeDidIdentifierPath(encoded) };
15
+ }
16
+ /**
17
+ * Handle one Universal Resolver request.
18
+ *
19
+ * @param context Request-owned host context with the configured runtime/registry.
20
+ * @param did Requested DID to resolve and verify.
21
+ * @returns HTTP resolution response based on accepted KERI/VDR state.
22
+ */
23
+ export async function handleDidResolutionRoute(context, did) {
24
+ const meta = wantsResolutionResult(context.req, context.url);
25
+ if (!context.runtime) {
26
+ return jsonResponse(didResolutionError("internalError", "DID resolver runtime is not available."), 500);
27
+ }
28
+ try {
29
+ const oobis = context.url.searchParams.getAll("oobi");
30
+ let result = null;
31
+ if (did.startsWith("did:webs:") || did.startsWith("did:web:")) {
32
+ const rgy = context.runtime.vdr;
33
+ if (!rgy)
34
+ throw new Error("Legacy credential registry is unavailable in this runtime.");
35
+ result = await run(() => resolveDidWebs(rgy, context.runtime.reactor, context.runtime.http, {
36
+ did,
37
+ metadata: meta,
38
+ insecureHttp: context.policy.dwsInsecureHttp ?? false,
39
+ }));
40
+ }
41
+ else if (did.startsWith("did:keri:")) {
42
+ const canonical = parseDidKeriIdentifier(did).canonical;
43
+ const rgy = context.runtime.vdr;
44
+ if (!rgy)
45
+ throw new Error("Legacy credential registry is unavailable in this runtime.");
46
+ // The host owns workers. This request only queues introductions and observes state.
47
+ for (const oobi of oobis)
48
+ context.runtime.oobiery.resolve(oobi);
49
+ const deadline = Date.now() + 30_000;
50
+ while (!oobis.every((url) => runtimeOobiConverged(context.runtime, url))) {
51
+ context.req.signal.throwIfAborted();
52
+ if (Date.now() >= deadline)
53
+ throw new Error("DID introductions did not converge within 30000ms.");
54
+ await new Promise((resolve) => setTimeout(resolve, 25));
55
+ }
56
+ result = resolveDidKeri(context.runtime.hby, rgy, canonical);
57
+ }
58
+ if (!result) {
59
+ return jsonResponse(didResolutionError("methodNotSupported", `Unsupported DID method for ${did}.`), meta ? 200 : 400);
60
+ }
61
+ return jsonResponse(meta ? result.resolution : result.document, 200, meta ? "application/did-resolution+json" : "application/did+json");
62
+ }
63
+ catch (error) {
64
+ return jsonResponse(didResolutionError("notFound", error instanceof Error ? error.message : String(error)), meta ? 200 : 404);
65
+ }
66
+ }
67
+ function wantsResolutionResult(req, url) {
68
+ if (url.searchParams.get("meta") === "true") {
69
+ return true;
70
+ }
71
+ return (req.headers.get("accept") ?? "").toLowerCase().includes("application/did-resolution");
72
+ }
73
+ function decodeDidIdentifierPath(encoded) {
74
+ if (encoded.startsWith("did:")) {
75
+ return encoded;
76
+ }
77
+ try {
78
+ const decoded = decodeURIComponent(encoded);
79
+ return decoded.startsWith("did:") ? decoded : encoded;
80
+ }
81
+ catch {
82
+ return encoded;
83
+ }
84
+ }
85
+ function jsonResponse(body, status = 200, contentType = "application/did+json") {
86
+ return new Response(`${JSON.stringify(body, null, 2)}\n`, {
87
+ status,
88
+ headers: { "Content-Type": contentType },
89
+ });
90
+ }
@@ -0,0 +1,112 @@
1
+ /** Static and dynamic `did:webs` artifact routes. */
2
+ import { didWebsFromArtifactRequest, generateDidWebsArtifacts, generateDidWebsJson, ValidationError, } from "keri-ts/runtime";
3
+ /** Classify configured `did.json` and `keri.cesr` artifact routes. */
4
+ export function classifyDwsArtifactRoute(context) {
5
+ if (context.method !== "GET") {
6
+ return null;
7
+ }
8
+ if (!context.policy.dwsDynamic && !context.policy.dwsStaticFilesDir) {
9
+ return null;
10
+ }
11
+ const segments = context.pathname.split("/").filter((part) => part.length > 0);
12
+ const configuredPath = didPathSegments(context.policy.dwsDidPath);
13
+ if (!pathPrefixMatches(segments, configuredPath)) {
14
+ return null;
15
+ }
16
+ const remaining = segments.slice(configuredPath.length);
17
+ if (remaining.length !== 2) {
18
+ return null;
19
+ }
20
+ const [aid, artifact] = remaining;
21
+ if (artifact !== "did.json" && artifact !== "keri.cesr") {
22
+ return null;
23
+ }
24
+ return { kind: "dwsArtifact", aid, artifact };
25
+ }
26
+ /** Serve one configured artifact route from dynamic state or static files. */
27
+ export async function handleDwsArtifactRoute(context, route, files) {
28
+ if (context.policy.dwsDynamic && context.runtime && dynamicAidAllowed(context, route.aid)) {
29
+ return dynamicArtifactResponse(context, route);
30
+ }
31
+ if (context.policy.dwsStaticFilesDir) {
32
+ return await staticArtifactResponse(context, route, files);
33
+ }
34
+ return new Response("Not Found", {
35
+ status: 404,
36
+ headers: { "Content-Type": "text/plain" },
37
+ });
38
+ }
39
+ function dynamicArtifactResponse(context, route) {
40
+ try {
41
+ const did = didWebsFromArtifactRequest({
42
+ host: context.req.headers.get("host") ?? context.url.host,
43
+ didPath: didPathSegments(context.policy.dwsDidPath),
44
+ aid: route.aid,
45
+ });
46
+ const rgy = context.runtime?.vdr;
47
+ if (!rgy)
48
+ throw new ValidationError("Legacy credential registry is unavailable in this runtime.");
49
+ const bytes = route.artifact === "did.json"
50
+ ? generateDidWebsJson(rgy, { did }).didJson
51
+ : generateDidWebsArtifacts(rgy, { did }).keriCesr;
52
+ return new Response(bodyBuffer(bytes), {
53
+ headers: {
54
+ "Content-Type": route.artifact === "did.json" ? "application/did+json" : "application/cesr",
55
+ },
56
+ });
57
+ }
58
+ catch (error) {
59
+ const message = error instanceof Error ? error.message : String(error);
60
+ return new Response(message, {
61
+ status: error instanceof ValidationError ? 400 : 500,
62
+ headers: { "Content-Type": "text/plain" },
63
+ });
64
+ }
65
+ }
66
+ async function staticArtifactResponse(context, route, files) {
67
+ const root = context.policy.dwsStaticFilesDir;
68
+ try {
69
+ const path = staticArtifactPath(root, context.policy.dwsDidPath, route.aid, route.artifact);
70
+ return await files.response(path, route.artifact === "did.json" ? "application/did+json" : "application/cesr", context.req.signal);
71
+ }
72
+ catch (error) {
73
+ if (error instanceof ValidationError) {
74
+ return new Response(error.message, {
75
+ status: 400,
76
+ headers: { "Content-Type": "text/plain" },
77
+ });
78
+ }
79
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
80
+ return new Response("Not Found", { status: 404, headers: { "Content-Type": "text/plain" } });
81
+ }
82
+ throw error;
83
+ }
84
+ }
85
+ function bodyBuffer(bytes) {
86
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
87
+ }
88
+ function dynamicAidAllowed(context, aid) {
89
+ const hosted = context.policy.hostedPrefixes ?? [];
90
+ return hosted.includes(aid);
91
+ }
92
+ function staticArtifactPath(root, didPath, aid, artifact) {
93
+ const normalizedRoot = root.replace(/\/+$/u, "");
94
+ const prefix = didPathSegments(didPath).map(safeSegment).join("/");
95
+ const parts = [normalizedRoot, prefix, safeSegment(aid), artifact].filter((part) => part.length > 0);
96
+ return parts.join("/");
97
+ }
98
+ function didPathSegments(path) {
99
+ return (path ?? "").split("/").filter((part) => part.length > 0);
100
+ }
101
+ function pathPrefixMatches(segments, prefix) {
102
+ if (segments.length < prefix.length) {
103
+ return false;
104
+ }
105
+ return prefix.every((part, index) => segments[index] === part);
106
+ }
107
+ function safeSegment(segment) {
108
+ if (segment.includes("/") || segment === ".." || segment.includes("\0")) {
109
+ throw new ValidationError(`Unsafe path segment ${segment}.`);
110
+ }
111
+ return segment;
112
+ }