@awiki/dsh-plugin 0.2.2 → 0.2.4

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 CHANGED
@@ -36,9 +36,16 @@ group administration, realtime push, or multiple attachments in one message.
36
36
  Install the official public npm package:
37
37
 
38
38
  ```bash
39
- pnpm add @awiki/dsh-plugin@latest
39
+ dsh plugin --profile web add @awiki/dsh-plugin@latest
40
40
  ```
41
41
 
42
+ The profile installer both adds the package and activates its bundle layer. A
43
+ plain `npm i @awiki/dsh-plugin` in a DSH project only installs the package; it
44
+ does not activate the bundle, so the profile command remains the recommended
45
+ installation path. This release line targets the `0.1.0-rc.7` package family
46
+ and pins every direct Host peer exactly, preventing npm from mixing prerelease
47
+ families in a DSH root dependency tree.
48
+
42
49
  `@awiki/dsh-plugin` is the canonical package identity starting with
43
50
  `0.2.0-rc.4`. The former `@awiki/dsh` registry entry was unpublished and is
44
51
  not an installation source for this release line.
@@ -107,6 +114,37 @@ stale, without another model call, when newer messages arrive. The replaceable
107
114
  for one direct `ctx.llm.stream` request; it does not create an Agent or write an
108
115
  Agent session.
109
116
 
117
+ ## External HTTP ANP authentication
118
+
119
+ Trusted same-process DSH Host plugins can authenticate an externally transported HTTP request
120
+ without handling ANP signatures, access tokens, challenges, or retries themselves:
121
+
122
+ ```ts
123
+ const response = await ctx.awiki.externalHttpAuth.dispatch(
124
+ new Request('https://api.example.com/orders', {
125
+ method: 'POST',
126
+ headers: { 'content-type': 'application/json' },
127
+ body: JSON.stringify({ productId: '123' }),
128
+ }),
129
+ request => fetch(request),
130
+ )
131
+ ```
132
+
133
+ The callback remains the only network transport owner. AWiki buffers at most 4 MiB of exact body
134
+ bytes, forces manual redirects, asks Rust to select an origin-scoped in-memory Bearer token or a
135
+ fresh HTTP Message Signature, observes only authentication response headers, and invokes the
136
+ transport at most twice for one bounded `401` authentication retry. The final `Response` body is
137
+ untouched. Transport rejections preserve their original error identity.
138
+
139
+ The unsigned input must not contain `Authorization`, `Signature-Input`, `Signature`, or
140
+ `Content-Digest`. Production targets require HTTPS; test-only loopback HTTP uses the existing
141
+ `allowInsecureLoopbackForTesting` deployment gate. Tokens come only from successful
142
+ `Authentication-Info` responses, are scoped to the current identity/signing key/origin, and are
143
+ not persisted across Harness restarts.
144
+
145
+ `externalHttpAuth` is deliberately absent from Browser Remote, Agent tools, Typert Remote, and the
146
+ Web client bundle. Exposing it across an untrusted boundary would create a signing oracle.
147
+
110
148
  ## Development
111
149
 
112
150
  Requirements: Node.js 22.19+ (or 24+) and pnpm 11.7.
@@ -117,7 +155,7 @@ pnpm run verify
117
155
  pnpm pack --dry-run
118
156
  ```
119
157
 
120
- The production Host loads the exact `@awiki/im-core-node@0.1.2` runtime package;
158
+ The production Host loads the exact `@awiki/im-core-node@0.1.3` runtime package;
121
159
  the platform-specific native addon is selected through its optional dependencies
122
160
  and remains external to the JavaScript bundle. Consumers do not need Rust or an
123
161
  `awiki-cli-rs2` checkout. See `THIRD_PARTY_NOTICES.md` for provenance and
package/README.zh.md CHANGED
@@ -29,9 +29,14 @@ Rust 身份。
29
29
  安装公开发布的官方 npm 包:
30
30
 
31
31
  ```bash
32
- pnpm add @awiki/dsh-plugin@latest
32
+ dsh plugin --profile web add @awiki/dsh-plugin@latest
33
33
  ```
34
34
 
35
+ Profile 安装器会同时添加包并激活 bundle layer。在 DSH 项目根目录执行普通的
36
+ `npm i @awiki/dsh-plugin` 只会安装依赖,不会激活 bundle,因此仍推荐使用上述
37
+ Profile 命令。本发布线面向 `0.1.0-rc.7` 包族,并精确锁定所有直接 Host peer,
38
+ 防止 npm 在 DSH 根依赖树中混用不同的预发布版本族。
39
+
35
40
  从 `0.2.0-rc.4` 起,`@awiki/dsh-plugin` 是唯一规范包名。原
36
41
  `@awiki/dsh` registry 条目已被 unpublish,不再作为本发布线的安装来源。
37
42
 
@@ -87,6 +92,35 @@ MIME、大小和说明,不发送文件二进制;序列化后的对话内容
87
92
  调用模型。可替换的 `@awiki/dsh-plugin/summary-provider` 使用 Harness 当前默认 provider/model
88
93
  执行一次直接的 `ctx.llm.stream`,不会创建 Agent,也不会写入 Agent session。
89
94
 
95
+ ## 外部 HTTP ANP 身份认证
96
+
97
+ 可信的 DSH Host 同进程插件可以认证由外部 transport 发送的 HTTP 请求,而无需自行处理
98
+ ANP 签名、Access Token、challenge 或重试:
99
+
100
+ ```ts
101
+ const response = await ctx.awiki.externalHttpAuth.dispatch(
102
+ new Request('https://api.example.com/orders', {
103
+ method: 'POST',
104
+ headers: { 'content-type': 'application/json' },
105
+ body: JSON.stringify({ productId: '123' }),
106
+ }),
107
+ request => fetch(request),
108
+ )
109
+ ```
110
+
111
+ 回调函数仍是唯一网络 transport owner。AWiki 最多缓冲 4 MiB 精确 body bytes,强制 manual
112
+ redirect,由 Rust 自动选择当前 origin 的进程内 Bearer Token 或新 HTTP Message Signature,
113
+ 只观察认证相关响应头,并且每个逻辑请求最多调用 transport 两次,第二次只能是一次受限的
114
+ `401` 认证重试。最终 `Response` 正文不会被读取;transport rejection 保留原始错误对象。
115
+
116
+ 输入请求不得自行携带 `Authorization`、`Signature-Input`、`Signature` 或
117
+ `Content-Digest`。生产目标必须使用 HTTPS;测试用 loopback HTTP 复用现有
118
+ `allowInsecureLoopbackForTesting` 部署开关。Token 只接受成功响应中的
119
+ `Authentication-Info`,并按当前 identity、signing key 和 origin 隔离;Harness 重启后不保留。
120
+
121
+ `externalHttpAuth` 不进入 Browser Remote、Agent tools、Typert Remote 或 Web client bundle,
122
+ 避免形成跨不可信边界的签名 oracle。
123
+
90
124
  ## 开发与验证
91
125
 
92
126
  需要 Node.js 22.19+(或 24+)以及 pnpm 11.7:
@@ -97,7 +131,7 @@ pnpm run verify
97
131
  pnpm pack --dry-run
98
132
  ```
99
133
 
100
- 生产 Host 加载固定版本 `@awiki/im-core-node@0.1.2`;平台原生 addon 由它的
134
+ 生产 Host 加载固定版本 `@awiki/im-core-node@0.1.3`;平台原生 addon 由它的
101
135
  optional dependencies 选择,并保持在 JavaScript bundle 外。使用者无需安装 Rust,
102
136
  也无需检出 `awiki-cli-rs2`。来源与许可证见 `THIRD_PARTY_NOTICES.md`。
103
137
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## AWiki Rust IM Core Node SDK
4
4
 
5
- `@awiki/im-core-node@0.1.2` and its target-specific optional package provide the
5
+ `@awiki/im-core-node@0.1.3` and its target-specific optional package provide the
6
6
  Rust IM Core runtime used by the Host provider. These packages are distributed
7
7
  under AGPL-3.0-only. Each package carries its own `LICENSE`, `NOTICE.md`,
8
8
  `SOURCE.md`, CycloneDX SBOM, checksums, and build provenance. The corresponding
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as downloadedAttachment } from "./sdk-adapter-DPPZhs6p.mjs";
1
+ import { n as downloadedAttachment } from "./sdk-adapter-DK6RqV-c.mjs";
2
2
  import "@deepseek-ai/cordis";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
@@ -14,6 +14,178 @@ const AWIKI_LOGOUT_CONFIRMATION = "logout-awiki-session";
14
14
  /** Exact browser acknowledgement required before destructive local-state removal. */
15
15
  const AWIKI_CLEAR_LOCAL_DATA_CONFIRMATION = "clear-awiki-local-data";
16
16
  //#endregion
17
+ //#region lib/types/external-http-auth.js
18
+ /** Host-only dispatcher for externally transported ANP-authenticated HTTP. */
19
+ const AWIKI_EXTERNAL_HTTP_MAX_BODY_BYTES = 4194304;
20
+ const MANAGED_HEADERS = [
21
+ "authorization",
22
+ "signature-input",
23
+ "signature",
24
+ "content-digest"
25
+ ];
26
+ const ERROR_MESSAGES = {
27
+ "not-registered": "A registered AWiki identity is required.",
28
+ "signed-out": "This installation is signed out of AWiki.",
29
+ "invalid-request": "The external HTTP request is invalid.",
30
+ "unsupported-body": "The external HTTP request body cannot be replayed safely.",
31
+ "body-too-large": "The external HTTP request body exceeds 4 MiB.",
32
+ "auth-state-unavailable": "AWiki external HTTP authentication is unavailable."
33
+ };
34
+ /** Stable Host-only failure without request, response, credential, or path detail. */
35
+ var AwikiExternalHttpAuthError = class extends Error {
36
+ code;
37
+ name = "AwikiExternalHttpAuthError";
38
+ constructor(code) {
39
+ super(ERROR_MESSAGES[code]);
40
+ this.code = code;
41
+ }
42
+ };
43
+ function createAwikiExternalHttpAuth(acquire) {
44
+ return Object.freeze({ async dispatch(request, transport) {
45
+ validateDispatchInput(request, transport);
46
+ const session = await acquire();
47
+ await session.assertActive();
48
+ const body = await readReplayableBody(request);
49
+ await session.assertActive();
50
+ let attempt;
51
+ try {
52
+ attempt = await session.client.prepareExternalHttpRequest({
53
+ url: request.url,
54
+ method: request.method,
55
+ headers: requestHeaders(request),
56
+ ...body === void 0 ? {} : { body }
57
+ });
58
+ } catch (error) {
59
+ throw mapProviderError(error);
60
+ }
61
+ await session.assertActive();
62
+ const response = await transport(authenticatedRequest(request, body, attempt));
63
+ const retry = await handleResponseWithoutChangingCompletedRequest(attempt, response);
64
+ if (retry === null) return response;
65
+ try {
66
+ await session.assertActive();
67
+ } catch {
68
+ return response;
69
+ }
70
+ const retriedResponse = await transport(authenticatedRequest(request, body, retry));
71
+ await handleResponseWithoutChangingCompletedRequest(retry, retriedResponse);
72
+ return retriedResponse;
73
+ } });
74
+ }
75
+ function externalHttpAuthError(code) {
76
+ return new AwikiExternalHttpAuthError(code);
77
+ }
78
+ function mapProviderError(error) {
79
+ if (error instanceof AwikiExternalHttpAuthError) return error;
80
+ try {
81
+ if (typeof error === "object" && error !== null) {
82
+ const value = error;
83
+ if (value.name === "AwikiSdkError") {
84
+ if (value.code === "not-registered") return externalHttpAuthError("not-registered");
85
+ if (value.code === "invalid-request") return externalHttpAuthError("invalid-request");
86
+ }
87
+ }
88
+ } catch {}
89
+ return externalHttpAuthError("auth-state-unavailable");
90
+ }
91
+ function validateDispatchInput(request, transport) {
92
+ if (!(request instanceof Request) || typeof transport !== "function" || request.bodyUsed) throw externalHttpAuthError("invalid-request");
93
+ for (const name of MANAGED_HEADERS) if (request.headers.has(name)) throw externalHttpAuthError("invalid-request");
94
+ }
95
+ async function readReplayableBody(request) {
96
+ if (request.body === null) return void 0;
97
+ let clone;
98
+ try {
99
+ clone = request.clone();
100
+ } catch {
101
+ throw externalHttpAuthError("unsupported-body");
102
+ }
103
+ const stream = clone.body;
104
+ if (stream === null) return /* @__PURE__ */ new Uint8Array();
105
+ const reader = stream.getReader();
106
+ const chunks = [];
107
+ let length = 0;
108
+ try {
109
+ while (true) {
110
+ const result = await reader.read();
111
+ if (result.done) break;
112
+ length += result.value.byteLength;
113
+ if (length > 4194304) {
114
+ reader.cancel().catch(() => {});
115
+ request.body?.cancel().catch(() => {});
116
+ throw externalHttpAuthError("body-too-large");
117
+ }
118
+ chunks.push(Uint8Array.from(result.value));
119
+ }
120
+ } catch (error) {
121
+ if (error instanceof AwikiExternalHttpAuthError) throw error;
122
+ throw externalHttpAuthError("unsupported-body");
123
+ } finally {
124
+ reader.releaseLock();
125
+ }
126
+ const bytes = new Uint8Array(length);
127
+ let offset = 0;
128
+ for (const chunk of chunks) {
129
+ bytes.set(chunk, offset);
130
+ offset += chunk.byteLength;
131
+ }
132
+ return bytes;
133
+ }
134
+ function requestHeaders(request) {
135
+ return [...request.headers].map(([name, value]) => ({
136
+ name,
137
+ value
138
+ }));
139
+ }
140
+ function authenticatedRequest(original, body, attempt) {
141
+ const headers = new Headers(original.headers);
142
+ for (const header of attempt.headerPatch) headers.set(header.name, header.value);
143
+ const init = {
144
+ method: attempt.method,
145
+ headers,
146
+ redirect: "manual",
147
+ signal: original.signal,
148
+ cache: original.cache,
149
+ credentials: original.credentials,
150
+ integrity: original.integrity,
151
+ keepalive: original.keepalive,
152
+ mode: original.mode,
153
+ referrer: original.referrer,
154
+ referrerPolicy: original.referrerPolicy,
155
+ ...body === void 0 ? {} : { body: Uint8Array.from(body) }
156
+ };
157
+ try {
158
+ return new Request(attempt.targetUrl, init);
159
+ } catch {
160
+ throw externalHttpAuthError("invalid-request");
161
+ }
162
+ }
163
+ async function handleResponseWithoutChangingCompletedRequest(attempt, response) {
164
+ try {
165
+ return await attempt.handleResponse(responseMetadata(response));
166
+ } catch {
167
+ return null;
168
+ }
169
+ }
170
+ function responseMetadata(response) {
171
+ const headers = [];
172
+ for (const name of [
173
+ "authentication-info",
174
+ "www-authenticate",
175
+ "accept-signature"
176
+ ]) {
177
+ const value = response.headers.get(name);
178
+ if (value !== null) headers.push({
179
+ name,
180
+ value
181
+ });
182
+ }
183
+ return {
184
+ statusCode: response.status,
185
+ headers
186
+ };
187
+ }
188
+ //#endregion
17
189
  //#region lib/types/tools.js
18
190
  /** Model-facing AWiki read and approved-send tools. */
19
191
  /** Model tool that reads the public deployment identity. */
@@ -288,7 +460,7 @@ function unavailable() {
288
460
  return {
289
461
  ok: false,
290
462
  error: {
291
- code: "settings-not-exposed",
463
+ code: "settings-rejected",
292
464
  message: "AWiki settings are unavailable in this Host composition.",
293
465
  details: { ns: AWIKI_SETTINGS_NAMESPACE }
294
466
  }
@@ -1027,6 +1199,8 @@ let AwikiService = (() => {
1027
1199
  sessionRevision = 0;
1028
1200
  activeSummaryRequests = /* @__PURE__ */ new Set();
1029
1201
  summaryProvider;
1202
+ /** Trusted same-process external HTTP authentication dispatcher. Never Remote. */
1203
+ externalHttpAuth;
1030
1204
  /**
1031
1205
  * @param ctx - owning Host context.
1032
1206
  * @param config - service endpoints, SDK state path, and public limits.
@@ -1034,6 +1208,7 @@ let AwikiService = (() => {
1034
1208
  constructor(ctx, config) {
1035
1209
  super(ctx, "awiki");
1036
1210
  this.resolved = resolveConfig(config);
1211
+ this.externalHttpAuth = createAwikiExternalHttpAuth(() => this.acquireExternalHttpAuthSession());
1037
1212
  this.sessionStore = new AwikiSessionStore(this.resolved.stateRoot);
1038
1213
  this.startupUserServiceDomain = this.resolved.userServiceDomain;
1039
1214
  ctx.inject(["settings"], (settingsCtx) => {
@@ -1470,6 +1645,38 @@ let AwikiService = (() => {
1470
1645
  this.signedOut ??= await this.sessionStore.isSignedOut();
1471
1646
  return this.signedOut;
1472
1647
  }
1648
+ /** Bind one external-auth dispatch to the current provider and session revision. */
1649
+ async acquireExternalHttpAuthSession() {
1650
+ let signedOut;
1651
+ try {
1652
+ signedOut = await this.isSignedOut();
1653
+ } catch {
1654
+ throw externalHttpAuthError("auth-state-unavailable");
1655
+ }
1656
+ if (signedOut) throw externalHttpAuthError("signed-out");
1657
+ const revision = this.sessionRevision;
1658
+ const provider = this.provider;
1659
+ if (provider === void 0) throw externalHttpAuthError("auth-state-unavailable");
1660
+ let identity;
1661
+ try {
1662
+ identity = await provider.client.getIdentity();
1663
+ } catch (error) {
1664
+ throw mapProviderError(error);
1665
+ }
1666
+ if (identity === null) throw externalHttpAuthError("not-registered");
1667
+ return {
1668
+ client: provider.client,
1669
+ assertActive: async () => {
1670
+ if (this.provider !== provider || this.sessionRevision !== revision) throw externalHttpAuthError("auth-state-unavailable");
1671
+ try {
1672
+ if (await this.isSignedOut()) throw externalHttpAuthError("signed-out");
1673
+ } catch (error) {
1674
+ if (error instanceof AwikiExternalHttpAuthError) throw error;
1675
+ throw externalHttpAuthError("auth-state-unavailable");
1676
+ }
1677
+ }
1678
+ };
1679
+ }
1473
1680
  /** Serialize sign-in, sign-out, and destructive clear transitions. */
1474
1681
  mutateSession(operation) {
1475
1682
  const pending = this.sessionMutation.then(operation, operation);
@@ -1494,4 +1701,4 @@ function containsUnexpectedBinary(value, seen) {
1494
1701
  return false;
1495
1702
  }
1496
1703
  //#endregion
1497
- export { AWIKI_CLEAR_LOCAL_DATA_CONFIRMATION, AWIKI_DOMAIN_FIELD, AWIKI_HISTORY_TOOL, AWIKI_IDENTITY_STATUS_TOOL, AWIKI_LIST_CONVERSATIONS_TOOL, AWIKI_LOGOUT_CONFIRMATION, AWIKI_SEND_ATTACHMENT_TOOL, AWIKI_SEND_MESSAGE_TOOL, AWIKI_SETTINGS_NAMESPACE, AwikiService, AwikiService as default, AwikiSettingsSchema, Config, DEFAULT_ATTACHMENT_MAX_BYTES, DEFAULT_AWIKI_DOMAIN, DEFAULT_AWIKI_MESSAGE_SERVICE_DID, DEFAULT_AWIKI_SERVICE_URL, DEFAULT_POLL_INTERVAL_MS, DEFAULT_SUMMARY_MAX_INPUT_BYTES, MAX_SUMMARY_MESSAGES, normalizeAwikiDomain, validateAwikiSettings };
1704
+ export { AWIKI_CLEAR_LOCAL_DATA_CONFIRMATION, AWIKI_DOMAIN_FIELD, AWIKI_EXTERNAL_HTTP_MAX_BODY_BYTES, AWIKI_HISTORY_TOOL, AWIKI_IDENTITY_STATUS_TOOL, AWIKI_LIST_CONVERSATIONS_TOOL, AWIKI_LOGOUT_CONFIRMATION, AWIKI_SEND_ATTACHMENT_TOOL, AWIKI_SEND_MESSAGE_TOOL, AWIKI_SETTINGS_NAMESPACE, AwikiExternalHttpAuthError, AwikiService, AwikiService as default, AwikiSettingsSchema, Config, DEFAULT_ATTACHMENT_MAX_BYTES, DEFAULT_AWIKI_DOMAIN, DEFAULT_AWIKI_MESSAGE_SERVICE_DID, DEFAULT_AWIKI_SERVICE_URL, DEFAULT_POLL_INTERVAL_MS, DEFAULT_SUMMARY_MAX_INPUT_BYTES, MAX_SUMMARY_MESSAGES, normalizeAwikiDomain, validateAwikiSettings };
package/lib/provider.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as RustSdkAdapter } from "./sdk-adapter-DPPZhs6p.mjs";
1
+ import { t as RustSdkAdapter } from "./sdk-adapter-DK6RqV-c.mjs";
2
2
  import { openImCoreNodeClient } from "@awiki/im-core-node";
3
3
  //#region lib/types/provider.js
4
4
  /** Production AWiki provider backed by the versioned Rust IM Core Node bridge. */
@@ -15,7 +15,8 @@ function apply(ctx) {
15
15
  userServiceEndpoint: options.userServiceUrl,
16
16
  messageServiceEndpoint: options.messageServiceUrl,
17
17
  anpServiceEndpoint: options.messageServiceUrl,
18
- anpServiceDid: options.messageServiceDid
18
+ anpServiceDid: options.messageServiceDid,
19
+ externalHttpAllowInsecureLoopbackForTesting: options.allowInsecureLoopbackForTesting
19
20
  }))), "awiki Rust SDK client");
20
21
  }
21
22
  //#endregion
@@ -111,6 +111,34 @@ function page(value, copy) {
111
111
  hasMore: value.hasMore
112
112
  };
113
113
  }
114
+ function httpHeaders(headers) {
115
+ return headers.map((header) => ({
116
+ name: String(header.name),
117
+ value: String(header.value)
118
+ }));
119
+ }
120
+ function externalHttpAttempt(value) {
121
+ return {
122
+ targetUrl: String(value.targetUrl),
123
+ method: String(value.method),
124
+ headerPatch: httpHeaders(value.headerPatch),
125
+ retryCount: value.retryCount,
126
+ async handleResponse(response) {
127
+ try {
128
+ const retry = await value.handleResponse({
129
+ statusCode: response.statusCode,
130
+ headers: response.headers.map((header) => ({
131
+ name: header.name,
132
+ value: header.value
133
+ }))
134
+ });
135
+ return retry === null ? null : externalHttpAttempt(retry);
136
+ } catch (error) {
137
+ mapError(error);
138
+ }
139
+ }
140
+ };
141
+ }
114
142
  /** Adapt the Rust Node bridge to the frozen Host provider interface. */
115
143
  var RustSdkAdapter = class {
116
144
  client;
@@ -204,6 +232,17 @@ var RustSdkAdapter = class {
204
232
  }
205
233
  fail("not-found");
206
234
  }
235
+ prepareExternalHttpRequest(request) {
236
+ return this.run(async (client) => externalHttpAttempt(await client.prepareExternalHttpRequest({
237
+ url: request.url,
238
+ method: request.method,
239
+ headers: request.headers.map((header) => ({
240
+ name: header.name,
241
+ value: header.value
242
+ })),
243
+ ...request.body === void 0 ? {} : { body: Uint8Array.from(request.body) }
244
+ })));
245
+ }
207
246
  getIdentity() {
208
247
  return this.run(async (client) => {
209
248
  const value = await client.getDefaultIdentity();
@@ -0,0 +1,23 @@
1
+ /** Host-only dispatcher for externally transported ANP-authenticated HTTP. */
2
+ import type { AwikiSdkClient } from './provider-api.ts';
3
+ export declare const AWIKI_EXTERNAL_HTTP_MAX_BODY_BYTES: number;
4
+ export type AwikiExternalHttpAuthErrorCode = 'not-registered' | 'signed-out' | 'invalid-request' | 'unsupported-body' | 'body-too-large' | 'auth-state-unavailable';
5
+ /** Stable Host-only failure without request, response, credential, or path detail. */
6
+ export declare class AwikiExternalHttpAuthError extends Error {
7
+ readonly code: AwikiExternalHttpAuthErrorCode;
8
+ readonly name = "AwikiExternalHttpAuthError";
9
+ constructor(code: AwikiExternalHttpAuthErrorCode);
10
+ }
11
+ export type AwikiHttpTransport = (request: Request) => Promise<Response>;
12
+ /** Trusted same-process API; never expose this interface through Remote or tools. */
13
+ export interface AwikiExternalHttpAuth {
14
+ dispatch(request: Request, transport: AwikiHttpTransport): Promise<Response>;
15
+ }
16
+ export interface AwikiExternalHttpAuthSession {
17
+ readonly client: AwikiSdkClient;
18
+ assertActive(): Promise<void>;
19
+ }
20
+ export declare function createAwikiExternalHttpAuth(acquire: () => Promise<AwikiExternalHttpAuthSession>): AwikiExternalHttpAuth;
21
+ export declare function externalHttpAuthError(code: AwikiExternalHttpAuthErrorCode): AwikiExternalHttpAuthError;
22
+ export declare function mapProviderError(error: unknown): AwikiExternalHttpAuthError;
23
+ //# sourceMappingURL=external-http-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"external-http-auth.d.ts","sourceRoot":"","sources":["../../src/external-http-auth.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAE9E,OAAO,KAAK,EACV,cAAc,EAIf,MAAM,mBAAmB,CAAA;AAE1B,eAAO,MAAM,kCAAkC,QAAkB,CAAA;AASjE,MAAM,MAAM,8BAA8B,GACtC,gBAAgB,GAChB,YAAY,GACZ,iBAAiB,GACjB,kBAAkB,GAClB,gBAAgB,GAChB,wBAAwB,CAAA;AAW5B,sFAAsF;AACtF,qBAAa,0BAA2B,SAAQ,KAAK;aAGhB,IAAI,EAAE,8BAA8B;IAFvE,SAAgB,IAAI,gCAA+B;gBAEhB,IAAI,EAAE,8BAA8B;CAGxE;AAED,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;AAExE,qFAAqF;AACrF,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,kBAAkB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CAC7E;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CAC9B;AAED,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,MAAM,OAAO,CAAC,4BAA4B,CAAC,GACnD,qBAAqB,CAmCvB;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,8BAA8B,GAAG,0BAA0B,CAEtG;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,0BAA0B,CAY3E"}
@@ -0,0 +1,185 @@
1
+ /** Host-only dispatcher for externally transported ANP-authenticated HTTP. */
2
+ export const AWIKI_EXTERNAL_HTTP_MAX_BODY_BYTES = 4 * 1024 * 1024;
3
+ const MANAGED_HEADERS = [
4
+ 'authorization',
5
+ 'signature-input',
6
+ 'signature',
7
+ 'content-digest',
8
+ ];
9
+ const ERROR_MESSAGES = {
10
+ 'not-registered': 'A registered AWiki identity is required.',
11
+ 'signed-out': 'This installation is signed out of AWiki.',
12
+ 'invalid-request': 'The external HTTP request is invalid.',
13
+ 'unsupported-body': 'The external HTTP request body cannot be replayed safely.',
14
+ 'body-too-large': 'The external HTTP request body exceeds 4 MiB.',
15
+ 'auth-state-unavailable': 'AWiki external HTTP authentication is unavailable.',
16
+ };
17
+ /** Stable Host-only failure without request, response, credential, or path detail. */
18
+ export class AwikiExternalHttpAuthError extends Error {
19
+ code;
20
+ name = 'AwikiExternalHttpAuthError';
21
+ constructor(code) {
22
+ super(ERROR_MESSAGES[code]);
23
+ this.code = code;
24
+ }
25
+ }
26
+ export function createAwikiExternalHttpAuth(acquire) {
27
+ return Object.freeze({
28
+ async dispatch(request, transport) {
29
+ validateDispatchInput(request, transport);
30
+ const session = await acquire();
31
+ await session.assertActive();
32
+ const body = await readReplayableBody(request);
33
+ await session.assertActive();
34
+ let attempt;
35
+ try {
36
+ attempt = await session.client.prepareExternalHttpRequest({
37
+ url: request.url,
38
+ method: request.method,
39
+ headers: requestHeaders(request),
40
+ ...body === undefined ? {} : { body },
41
+ });
42
+ }
43
+ catch (error) {
44
+ throw mapProviderError(error);
45
+ }
46
+ await session.assertActive();
47
+ const response = await transport(authenticatedRequest(request, body, attempt));
48
+ const retry = await handleResponseWithoutChangingCompletedRequest(attempt, response);
49
+ if (retry === null)
50
+ return response;
51
+ try {
52
+ await session.assertActive();
53
+ }
54
+ catch {
55
+ return response;
56
+ }
57
+ const retriedResponse = await transport(authenticatedRequest(request, body, retry));
58
+ await handleResponseWithoutChangingCompletedRequest(retry, retriedResponse);
59
+ return retriedResponse;
60
+ },
61
+ });
62
+ }
63
+ export function externalHttpAuthError(code) {
64
+ return new AwikiExternalHttpAuthError(code);
65
+ }
66
+ export function mapProviderError(error) {
67
+ if (error instanceof AwikiExternalHttpAuthError)
68
+ return error;
69
+ try {
70
+ if (typeof error === 'object' && error !== null) {
71
+ const value = error;
72
+ if (value.name === 'AwikiSdkError') {
73
+ if (value.code === 'not-registered')
74
+ return externalHttpAuthError('not-registered');
75
+ if (value.code === 'invalid-request')
76
+ return externalHttpAuthError('invalid-request');
77
+ }
78
+ }
79
+ }
80
+ catch { }
81
+ return externalHttpAuthError('auth-state-unavailable');
82
+ }
83
+ function validateDispatchInput(request, transport) {
84
+ if (!(request instanceof Request) || typeof transport !== 'function' || request.bodyUsed) {
85
+ throw externalHttpAuthError('invalid-request');
86
+ }
87
+ for (const name of MANAGED_HEADERS) {
88
+ if (request.headers.has(name))
89
+ throw externalHttpAuthError('invalid-request');
90
+ }
91
+ }
92
+ async function readReplayableBody(request) {
93
+ if (request.body === null)
94
+ return undefined;
95
+ let clone;
96
+ try {
97
+ clone = request.clone();
98
+ }
99
+ catch {
100
+ throw externalHttpAuthError('unsupported-body');
101
+ }
102
+ const stream = clone.body;
103
+ if (stream === null)
104
+ return new Uint8Array();
105
+ const reader = stream.getReader();
106
+ const chunks = [];
107
+ let length = 0;
108
+ try {
109
+ while (true) {
110
+ const result = await reader.read();
111
+ if (result.done)
112
+ break;
113
+ length += result.value.byteLength;
114
+ if (length > AWIKI_EXTERNAL_HTTP_MAX_BODY_BYTES) {
115
+ void reader.cancel().catch(() => { });
116
+ void request.body?.cancel().catch(() => { });
117
+ throw externalHttpAuthError('body-too-large');
118
+ }
119
+ chunks.push(Uint8Array.from(result.value));
120
+ }
121
+ }
122
+ catch (error) {
123
+ if (error instanceof AwikiExternalHttpAuthError)
124
+ throw error;
125
+ throw externalHttpAuthError('unsupported-body');
126
+ }
127
+ finally {
128
+ reader.releaseLock();
129
+ }
130
+ const bytes = new Uint8Array(length);
131
+ let offset = 0;
132
+ for (const chunk of chunks) {
133
+ bytes.set(chunk, offset);
134
+ offset += chunk.byteLength;
135
+ }
136
+ return bytes;
137
+ }
138
+ function requestHeaders(request) {
139
+ return [...request.headers].map(([name, value]) => ({ name, value }));
140
+ }
141
+ function authenticatedRequest(original, body, attempt) {
142
+ const headers = new Headers(original.headers);
143
+ for (const header of attempt.headerPatch)
144
+ headers.set(header.name, header.value);
145
+ const init = {
146
+ method: attempt.method,
147
+ headers,
148
+ redirect: 'manual',
149
+ signal: original.signal,
150
+ cache: original.cache,
151
+ credentials: original.credentials,
152
+ integrity: original.integrity,
153
+ keepalive: original.keepalive,
154
+ mode: original.mode,
155
+ referrer: original.referrer,
156
+ referrerPolicy: original.referrerPolicy,
157
+ ...body === undefined ? {} : { body: Uint8Array.from(body) },
158
+ };
159
+ try {
160
+ return new Request(attempt.targetUrl, init);
161
+ }
162
+ catch {
163
+ throw externalHttpAuthError('invalid-request');
164
+ }
165
+ }
166
+ async function handleResponseWithoutChangingCompletedRequest(attempt, response) {
167
+ try {
168
+ return await attempt.handleResponse(responseMetadata(response));
169
+ }
170
+ catch {
171
+ // The transport has already completed and may have executed a non-idempotent
172
+ // operation. Never turn post-response auth bookkeeping into a replay signal.
173
+ return null;
174
+ }
175
+ }
176
+ function responseMetadata(response) {
177
+ const headers = [];
178
+ for (const name of ['authentication-info', 'www-authenticate', 'accept-signature']) {
179
+ const value = response.headers.get(name);
180
+ if (value !== null)
181
+ headers.push({ name, value });
182
+ }
183
+ return { statusCode: response.status, headers };
184
+ }
185
+ //# sourceMappingURL=external-http-auth.js.map