@awiki/dsh-plugin 0.2.2 → 0.2.3

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,17 @@ 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. Do
43
+ not add the plugin to the DSH CLI project root with npm or pnpm: profile-managed
44
+ plugins resolve Host packages from the running Harness installation. This
45
+ release line targets the `0.1.0-rc.6` package family and pins those Host peer
46
+ ranges exactly. The Harness profile sets `autoInstallPeers: false`, so an
47
+ external plugin never installs an `rc.7` Host package into an `rc.6`
48
+ deployment.
49
+
42
50
  `@awiki/dsh-plugin` is the canonical package identity starting with
43
51
  `0.2.0-rc.4`. The former `@awiki/dsh` registry entry was unpublished and is
44
52
  not an installation source for this release line.
@@ -107,6 +115,37 @@ stale, without another model call, when newer messages arrive. The replaceable
107
115
  for one direct `ctx.llm.stream` request; it does not create an Agent or write an
108
116
  Agent session.
109
117
 
118
+ ## External HTTP ANP authentication
119
+
120
+ Trusted same-process DSH Host plugins can authenticate an externally transported HTTP request
121
+ without handling ANP signatures, access tokens, challenges, or retries themselves:
122
+
123
+ ```ts
124
+ const response = await ctx.awiki.externalHttpAuth.dispatch(
125
+ new Request('https://api.example.com/orders', {
126
+ method: 'POST',
127
+ headers: { 'content-type': 'application/json' },
128
+ body: JSON.stringify({ productId: '123' }),
129
+ }),
130
+ request => fetch(request),
131
+ )
132
+ ```
133
+
134
+ The callback remains the only network transport owner. AWiki buffers at most 4 MiB of exact body
135
+ bytes, forces manual redirects, asks Rust to select an origin-scoped in-memory Bearer token or a
136
+ fresh HTTP Message Signature, observes only authentication response headers, and invokes the
137
+ transport at most twice for one bounded `401` authentication retry. The final `Response` body is
138
+ untouched. Transport rejections preserve their original error identity.
139
+
140
+ The unsigned input must not contain `Authorization`, `Signature-Input`, `Signature`, or
141
+ `Content-Digest`. Production targets require HTTPS; test-only loopback HTTP uses the existing
142
+ `allowInsecureLoopbackForTesting` deployment gate. Tokens come only from successful
143
+ `Authentication-Info` responses, are scoped to the current identity/signing key/origin, and are
144
+ not persisted across Harness restarts.
145
+
146
+ `externalHttpAuth` is deliberately absent from Browser Remote, Agent tools, Typert Remote, and the
147
+ Web client bundle. Exposing it across an untrusted boundary would create a signing oracle.
148
+
110
149
  ## Development
111
150
 
112
151
  Requirements: Node.js 22.19+ (or 24+) and pnpm 11.7.
@@ -117,7 +156,7 @@ pnpm run verify
117
156
  pnpm pack --dry-run
118
157
  ```
119
158
 
120
- The production Host loads the exact `@awiki/im-core-node@0.1.2` runtime package;
159
+ The production Host loads the exact `@awiki/im-core-node@0.1.3` runtime package;
121
160
  the platform-specific native addon is selected through its optional dependencies
122
161
  and remains external to the JavaScript bundle. Consumers do not need Rust or an
123
162
  `awiki-cli-rs2` checkout. See `THIRD_PARTY_NOTICES.md` for provenance and
package/README.zh.md CHANGED
@@ -29,9 +29,15 @@ 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 CLI 项目根目录用
36
+ npm 或 pnpm 添加插件;Profile 管理的插件会从当前 Harness 安装解析 Host 包。
37
+ 本发布线面向 `0.1.0-rc.6` 包族,并精确锁定这些 Host peer。Harness Profile
38
+ 设置了 `autoInstallPeers: false`,因此外部插件不会在 `rc.6` 部署中自动混入
39
+ `rc.7` Host 包。
40
+
35
41
  从 `0.2.0-rc.4` 起,`@awiki/dsh-plugin` 是唯一规范包名。原
36
42
  `@awiki/dsh` registry 条目已被 unpublish,不再作为本发布线的安装来源。
37
43
 
@@ -87,6 +93,35 @@ MIME、大小和说明,不发送文件二进制;序列化后的对话内容
87
93
  调用模型。可替换的 `@awiki/dsh-plugin/summary-provider` 使用 Harness 当前默认 provider/model
88
94
  执行一次直接的 `ctx.llm.stream`,不会创建 Agent,也不会写入 Agent session。
89
95
 
96
+ ## 外部 HTTP ANP 身份认证
97
+
98
+ 可信的 DSH Host 同进程插件可以认证由外部 transport 发送的 HTTP 请求,而无需自行处理
99
+ ANP 签名、Access Token、challenge 或重试:
100
+
101
+ ```ts
102
+ const response = await ctx.awiki.externalHttpAuth.dispatch(
103
+ new Request('https://api.example.com/orders', {
104
+ method: 'POST',
105
+ headers: { 'content-type': 'application/json' },
106
+ body: JSON.stringify({ productId: '123' }),
107
+ }),
108
+ request => fetch(request),
109
+ )
110
+ ```
111
+
112
+ 回调函数仍是唯一网络 transport owner。AWiki 最多缓冲 4 MiB 精确 body bytes,强制 manual
113
+ redirect,由 Rust 自动选择当前 origin 的进程内 Bearer Token 或新 HTTP Message Signature,
114
+ 只观察认证相关响应头,并且每个逻辑请求最多调用 transport 两次,第二次只能是一次受限的
115
+ `401` 认证重试。最终 `Response` 正文不会被读取;transport rejection 保留原始错误对象。
116
+
117
+ 输入请求不得自行携带 `Authorization`、`Signature-Input`、`Signature` 或
118
+ `Content-Digest`。生产目标必须使用 HTTPS;测试用 loopback HTTP 复用现有
119
+ `allowInsecureLoopbackForTesting` 部署开关。Token 只接受成功响应中的
120
+ `Authentication-Info`,并按当前 identity、signing key 和 origin 隔离;Harness 重启后不保留。
121
+
122
+ `externalHttpAuth` 不进入 Browser Remote、Agent tools、Typert Remote 或 Web client bundle,
123
+ 避免形成跨不可信边界的签名 oracle。
124
+
90
125
  ## 开发与验证
91
126
 
92
127
  需要 Node.js 22.19+(或 24+)以及 pnpm 11.7:
@@ -97,7 +132,7 @@ pnpm run verify
97
132
  pnpm pack --dry-run
98
133
  ```
99
134
 
100
- 生产 Host 加载固定版本 `@awiki/im-core-node@0.1.2`;平台原生 addon 由它的
135
+ 生产 Host 加载固定版本 `@awiki/im-core-node@0.1.3`;平台原生 addon 由它的
101
136
  optional dependencies 选择,并保持在 JavaScript bundle 外。使用者无需安装 Rust,
102
137
  也无需检出 `awiki-cli-rs2`。来源与许可证见 `THIRD_PARTY_NOTICES.md`。
103
138
 
@@ -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. */
@@ -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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"external-http-auth.js","sourceRoot":"","sources":["../../src/external-http-auth.ts"],"names":[],"mappings":"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"}