@deepseek-ai/dsh-typert-protocol 0.2.1-alpha.1 → 0.2.1-alpha.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.i18n.yaml CHANGED
@@ -18,8 +18,11 @@
18
18
  en: c479b5b665822198
19
19
  zh: 1ffb789c18043baf
20
20
  /deepseek-ai-dsh-typert-protocol/use-this-package/exposing-a-host-method:
21
- en: fae902875c18ef33
22
- zh: ed4332cabe0e3a45
21
+ en: 63968de5fc817d06
22
+ zh: fbe2804b2a2d70d3
23
+ /deepseek-ai-dsh-typert-protocol/use-this-package/identifying-the-caller-and-admitting-non-owner-callers:
24
+ en: 78e08ae732a8f6f4
25
+ zh: be15fb7599994fb9
23
26
  /deepseek-ai-dsh-typert-protocol/use-this-package/associating-host-objects-and-contexts-with-wire-identities:
24
27
  en: 3db384ae6f215632
25
28
  zh: 56207184b09d4018
@@ -36,17 +39,17 @@
36
39
  en: 4f45bb46a1920984
37
40
  zh: 70408d3b59d2735a
38
41
  /deepseek-ai-dsh-typert-protocol/understand-the-implementation/remote-markers:
39
- en: 93b83a9441a5a3c2
40
- zh: c4eb143ddd167a27
42
+ en: 082530713c5cea83
43
+ zh: 592fe5d9636ef288
41
44
  /deepseek-ai-dsh-typert-protocol/understand-the-implementation/protocol-maps-and-descriptors:
42
- en: 164b30b3fa543936
43
- zh: 0831edd3e04a3d3a
45
+ en: c202904201402211
46
+ zh: 3db1d83683b4fd2a
44
47
  /deepseek-ai-dsh-typert-protocol/understand-the-implementation/wire-identity-grammar:
45
48
  en: aac79a7b4682371e
46
49
  zh: bc9cc513c1ced3e6
47
50
  /deepseek-ai-dsh-typert-protocol/understand-the-implementation/source-map:
48
- en: 62da405f16379f39
49
- zh: fcbd0f10dd198018
51
+ en: 74fdb78bf4e7753b
52
+ zh: dad439ba2a323ff9
50
53
  /deepseek-ai-dsh-typert-protocol/further-exploration:
51
54
  en: 84191d1b07fb8bb6
52
55
  zh: 9a5212732ed38ab6
@@ -57,8 +60,8 @@
57
60
  en: b1c49d5e440d9d41
58
61
  zh: 87ead78472415189
59
62
  /deepseek-ai-dsh-typert-protocol/known-limitations-and-deferred-work:
60
- en: 0be59e6d97923487
61
- zh: 70523fb0dc567ed6
63
+ en: 32a7ede4de20ad4e
64
+ zh: 57b86c314cd376b8
62
65
  /deepseek-ai-dsh-typert-protocol/known-limitations-and-deferred-work/dev-note:
63
66
  en: e67f633dc6fe7773
64
67
  zh: 3bb0c20c93f16f98
package/README.md CHANGED
@@ -46,7 +46,21 @@ Generation turns the method into a wire endpoint under the service's namespace;
46
46
 
47
47
  A unary method can return `Uint8Array` directly or within nested objects, arrays, tuples, optional fields, unions, and recursive types. Generation supplies optional result codec `encode()` and `decode()` functions: encoding visits only subtrees whose types can contain bytes, while decoding validates reconstructed values; Client declarations use `Uint8Array<ArrayBuffer>` at every byte position while retaining other field types. Pure JSON results pass through without Host byte detection or Client parsing. Parameters, events, and stream items remain JSON-only; runtime object cycles are unsupported.
48
48
 
49
- A stream method (`@Remote({ mode: 'stream' })`) returns `Iterable`, `AsyncIterable`, or `RemoteStream<Out, In>`. `In` declares the items the Client may send back on the same logical stream; the method reads them through `this.ctx.invocation.uplink<In>()`, and the descriptor carries their codec. `RemoteInvocation` also names the receiving `service`, the calling `peer` (a `PeerScope` the connection layer admitted), and the carrier `signal`; `ctx.invocation` is `undefined` on a Context no Remote call derived: the first `bindTypertRemote()` binding in a tree, which every `TypertRemoteService` constructor makes, registers that accessor on the root. A generated Client stream method returns `RemoteStreamHandle<Out, In>`: the handle with `send`, `end`, and `dispose` beside the downlink iteration. Uplink items are validated one by one at the Host because they arrive from the browser; downlink items are values the Host method produced and pass through.
49
+ A stream method (`@Remote({ mode: 'stream' })`) returns `Iterable`, `AsyncIterable`, or `RemoteStream<Out, In>`. `In` declares the items the Client may send back on the same logical stream; the method reads them through `this.ctx.invocation.uplink<In>()`, and the descriptor carries their codec. `RemoteInvocation` also names the receiving `service`, the calling `peer` (the `PeerScope` its carrier names), and the carrier `signal`; `ctx.invocation` is `undefined` on a Context no Remote call derived: the first `bindTypertRemote()` binding in a tree, which every `TypertRemoteService` constructor makes, registers that accessor on the root. Cordis's cross-copy `Context.is()` recognizes a source Context passed to a built Service, so mixed source/built compositions retain this accessor and invocation-local principals. A generated Client stream method returns `RemoteStreamHandle<Out, In>`: the handle with `send`, `end`, and `dispose` beside the downlink iteration. Uplink items are validated one by one at the Host because they arrive from the browser; downlink items are values the Host method produced and pass through.
50
+
51
+ `@Remote` options contain only `name`, `mode`, and the `resources` readers described below; input policy belongs to the consumer, not a method annotation. `method.input(options?: TypertRemoteInputOptions)` returns a cached `TypertRemoteValue<Input>`, with `strict?: boolean` as its only option. `method.input()` and `{ strict: false }` retain ordinary Remote handling: declared objects accept and strip unknown fields. `{ strict: true }` rejects undeclared fields recursively, while explicit JSON values and dictionary keys stay open and dictionary values retain their declared validation. Both named-argument envelopes reject extra arguments. Select one view (`const input = method.input({ strict: true })`) for matching `input.create().parse(raw)` and `input.jsonSchema`; this does not change Host dispatch. The [generator](<../generator/README.md#ordinary-remote-metadata>) owns publication and the paired codecs.
52
+
53
+ Ordinary `./remote` artifacts export `TYPERT_REMOTE` as a `TypertRemoteInfo`, extending the mountable `TypertRemoteContribution`. Its typed `methods[endpoint]` entries are the same direct descriptor objects found in `descriptors`, with derived `input(options?)` and `invoke(remote, input, signal?)`. Parameter documentation lives in `parameters[].description`; `result` is the single result codec, parser, and optional projection. Unary descriptors omit `mode`; streams use `'stream'`. Context-only receivers remain in `descriptors` and scoped declarations, not `methods`.
54
+
55
+ `createTypertRemoteInfo(spec, z)` from the published pure `@deepseek-ai/dsh-typert-protocol/remote-info` entry returns a class instance, not a constructor. Construction retains only the static spec and the caller-supplied Zod runtime; the first `descriptors` or `methods` read builds and caches their shared index. Each input-policy view, its parser, and its optional JSON Schema projection are independently lazy and cached; `method.input().create()` requests the ordinary parser. The raw spec requires Zod factories, while consumer-facing `TypertSchema<T>` promises only `parse(value: unknown): T`. JSON Schema comes from generator projections, not runtime `z.toJSONSchema()`. The leaf has no runtime imports; generated artifacts supply Zod. Metadata and schema access never invokes the network; the root also re-exports the factory but imports Cordis.
56
+
57
+ The invoker takes a caller-owned, mounted, unscoped Client Remote and resolves its namespace method at call time. It preserves the namespace receiver, optional positional holes, and signal, forwarding the same `Promise<RemoteResult<DecodedOutput>>` or complete `RemoteStreamHandle<Output, Uplink>` without an async wrapper. `DecodedOutput` defaults to the authored output; generation supplies `RemoteDecoded<Output>` only for byte-bearing unary methods, avoiding recursive remapping of ordinary JSON results; `result.create().parse()` retains the authored output type and does not normalize shared byte buffers. Documentation and parsing remain available when either input variant or the result lacks JSON Schema support; each variant's `jsonSchemaError` explains its absent projection. Registry readers use `isTypertRemoteMethod(descriptor)` from the same pure entry to narrow a base descriptor to these direct capabilities.
58
+
59
+ ### Identifying the caller and admitting non-owner callers
60
+
61
+ `PeerScope.principal` is the caller identity that in-process Host code fixes when it creates the Peer; no wire field can set it, and `PeerScope.signal` aborts when the Peer scope ends. `PrincipalMap` is merge-extensible: each driver declares one JSON entry with a unique literal `kind` and no credential, because a principal can be persisted in a Session log; the built-in entry is `owner: { kind: 'owner' }`. A Remote method that records its caller on durable output calls `principalOf(this.ctx.invocation)`, which returns the principal of a non-owner caller and `undefined` for the owner and outside a Remote call, and writes the field only when it is defined.
62
+
63
+ Every Remote method is under access control: [`dsh-api-remote-access`](../../api/remote-access/README.md) decides whether a non-owner caller may reach it on the resources the call touches. `TypertResourceMap` is merge-extensible beside `TypertLookupMap`: the package that owns an identity type declares its resource kind as `TypertResource<Id>` (`dsh-session` declares `session: TypertResource<SessionId>`, `dsh-workspace` declares `workspace: TypertResource<WorkspaceId>`). A `RemoteAccessResource` is either the built-in identity-less `{ kind: 'global' }` or one declared kind with its identity; resources are independent, so access to `global` implies nothing about any session. At runtime the domain that owns a kind registers its default wire field through `typert.resources.register(kind, { field })`, which rejects `global`; a lookup provider and a Host Context adapter name the kind their wire identity carries through `resource`, so a method with an Agent lookup parameter or an Agent-scoped receiver carries a `session` resource without options. The `resources` option of `@Remote` and `@RemoteScope` maps a kind to a reader that receives the method's wire arguments as its own parameters; their types are inferred from the decorated method, with lookup parameters mapped to their wire types, so `@Remote({ name: 'page', resources: { session: request => request.address.sessionId } })` reads a field the defaults cannot. A call whose readers and defaults name no resource touches `global`; they must name every resource the method accesses, because access control never checks one they omit. A forwarded `emit` entry of `TypertForwardableEventEntry` declares `resources` readers typed by its own event's parameters, and an entry without them touches `global`. Resource resolution belongs to the [API Gateway](../../api/gateway/README.md); decisions and refusal reasons belong to [`dsh-api-remote-access`](../../api/remote-access/README.md).
50
64
 
51
65
  ### Associating Host objects and Contexts with wire identities
52
66
 
@@ -89,11 +103,15 @@ The package keeps strict reflection in the compiler: decorator initializers reta
89
103
 
90
104
  ### Remote markers
91
105
 
92
- `@Remote` and `@RemoteScope` schedule an initializer that appends the method name, an optional export name, and the invocation mode to the prototype descriptor; `remoteMethods(service)` validates its version and returns a detached declaration-order snapshot that the Gateway's source-mode fallback reads. Markers require public, non-static instance methods with string names, and conflicting markers on one method are rejected.
106
+ `@Remote` and `@RemoteScope` schedule an initializer that appends the method name, an optional export name, the invocation mode, and the frozen `resources` readers to the prototype descriptor; `remoteMethods(service)` validates its version and returns a detached declaration-order snapshot that the Gateway's source-mode fallback reads. Markers require public, non-static instance methods with string names, and conflicting markers on one method are rejected.
107
+
108
+ Readers are validated when the decorator is created and frozen into one record per decorator application; the Gateway reads them from `remoteMethods(service)` by implementation member name, so another installed copy of the protocol package reads the same record.
93
109
 
94
110
  ### Protocol maps and descriptors
95
111
 
96
- The merge-extensible protocol maps keep static associations in the type system, while runtime providers register resolution with `ctx.typert`; the map names and shapes live in [`src/types.ts`](src/types.ts). `InvocationDescriptor` is the shared runtime form consumed by the registry, the Gateway, and the Client Remote, covering direct and Context receivers, JSON and lookup parameters, scope projections, the uplink codec, cancellation, and result codecs.
112
+ The merge-extensible protocol maps keep static associations in the type system, while runtime providers register resolution with `ctx.typert`; the map names and shapes live in [`src/types.ts`](src/types.ts). `InvocationDescriptor` is the shared runtime form consumed by the registry, the Gateway, and the Client Remote, covering direct and Context receivers, JSON and lookup parameters, scope projections, the uplink codec, cancellation, result codecs, and documentation. Generated `TypertRemoteMethod` objects extend this descriptor directly.
113
+
114
+ `RemoteInvocation.uplinkWindow` optionally describes the carrier's complete-frame inbox capacity and the UTF-8 envelope cost added to compact JSON items. Its absence means the carrier has no encoded-frame inbox limit; consumers still own their local buffering and acknowledgement policy.
97
115
 
98
116
  ### Wire identity grammar
99
117
 
@@ -104,9 +122,12 @@ Every namespace, method, lookup, and Context segment must satisfy `isTypertRemot
104
122
  | File | Role |
105
123
  |---|---|
106
124
  | [`src/index.ts`](src/index.ts) | Decorators, Gateway bindings, `remoteMethods`, segment validation |
125
+ | [`src/principal.ts`](src/principal.ts) | `principalOf`, the caller principal a Remote-call producer records |
126
+ | [`src/remote-access.ts`](src/remote-access.ts) | `RemoteAccessResource`, `RemoteAccessResourceKind`, and `RemoteAccessResourceOf` over `TypertResourceMap` |
107
127
  | [`src/json-value.ts`](src/json-value.ts) | `isRemoteJsonValue` and `isRemoteUplinkItem`, the lossless JSON checks every carrier shares |
108
128
  | [`src/remote-error.ts`](src/remote-error.ts) | `RemoteError` and the structural `remoteErrorOf` recognizer |
109
- | [`src/types.ts`](src/types.ts) | Protocol maps, `RemoteErrorDetailsMap`, `RemoteResult`, `RemoteStream`, `RemoteStreamHandle`, `PeerScope`, `RemoteInvocation`, `InvocationDescriptor`, codecs, provider contracts, registry interfaces, `TypertClientRemote` |
129
+ | [`src/remote-info.ts`](src/remote-info.ts) | Pure lazy info factory and direct-method type guard |
130
+ | [`src/types.ts`](src/types.ts) | Protocol maps, `RemoteErrorDetailsMap`, `RemoteResult`, `RemoteStream`, `RemoteStreamHandle`, `PrincipalMap`, `Principal`, `PeerScope`, `RemoteInvocation`, `InvocationDescriptor`, codecs, provider contracts, registry interfaces, `TypertClientRemote` |
110
131
 
111
132
  </details>
112
133
 
@@ -141,7 +162,7 @@ No direct effect; the declared contracts reach a request only when an assembly p
141
162
 
142
163
  These limits define what the declarations can represent; they are current package constraints, not a task backlog.
143
164
 
144
- - **Decorator markers are minimal** — markers contain only the method name and the direct or Context invocation mode; parameter, result, lookup, and schema reflection require the Typert build pipeline.
165
+ - **Decorator markers are minimal** — markers contain the method name, optional export alias, stream mode, resource readers, and direct or Context receiver selection; parameter, result, lookup, schema, and documentation metadata require the Typert build pipeline.
145
166
  - **Remote signatures are restricted** — decorators accept only public, non-static instance methods with string names, and source-mode execution cannot represent overloaded, destructured, defaulted, or rest-parameter signatures.
146
167
 
147
168
  <a id="dev-note"></a>
package/README.zh.md CHANGED
@@ -46,7 +46,21 @@ export class GoalService extends TypertRemoteService {
46
46
 
47
47
  一元方法可以直接返回 `Uint8Array`,也可以将其放在嵌套对象、数组、元组、可选字段、联合类型和递归类型中。生成器提供可选的结果 codec `encode()` 和 `decode()`:编码仅访问类型可能包含字节的子树,解码则校验还原后的值;Client 声明在每个字节位置使用 `Uint8Array<ArrayBuffer>`,同时保留其他字段类型。纯 JSON 结果不经 Host 字节识别或 Client 解析直接传递。参数、事件与流条目仍仅支持 JSON;不支持运行时对象循环。
48
48
 
49
- 流方法(`@Remote({ mode: 'stream' })`)返回 `Iterable`、`AsyncIterable` 或 `RemoteStream<Out, In>`。`In` 声明 Client 可以在同一条逻辑流上回送的项;方法通过 `this.ctx.invocation.uplink<In>()` 读取它们,描述符携带其 codec。`RemoteInvocation` 还给出接收服务 `service`、发起调用的 `peer`(连接层接纳的一个 `PeerScope`)与载体 `signal`;非 Remote 调用派生的 Context 上 `ctx.invocation` 为 `undefined`:树中第一个 `bindTypertRemote()` 绑定(每个 `TypertRemoteService` 构造函数都会建立一个)在根上注册该 accessor。生成的 Client 流方法返回 `RemoteStreamHandle<Out, In>`:在下行迭代之外提供 `send`、`end` 与 `dispose` 的句柄。上行项在 Host 逐项校验,因为它们来自浏览器;下行项是 Host 方法产出的值,原样透传。
49
+ 流方法(`@Remote({ mode: 'stream' })`)返回 `Iterable`、`AsyncIterable` 或 `RemoteStream<Out, In>`。`In` 声明 Client 可以在同一条逻辑流上回送的项;方法通过 `this.ctx.invocation.uplink<In>()` 读取它们,描述符携带其 codec。`RemoteInvocation` 还给出接收服务 `service`、发起调用的 `peer`(其载体指明的 `PeerScope`)与载体 `signal`;非 Remote 调用派生的 Context 上 `ctx.invocation` 为 `undefined`:树中第一个 `bindTypertRemote()` 绑定(每个 `TypertRemoteService` 构造函数都会建立一个)在根上注册该 accessor。Cordis 的跨副本 `Context.is()` 可识别传给构建产物 Service 的源码 Context,因此混合源码/构建组合仍保留此 accessor 和调用局部的 principal。生成的 Client 流方法返回 `RemoteStreamHandle<Out, In>`:在下行迭代之外提供 `send`、`end` 与 `dispose` 的句柄。上行项在 Host 逐项校验,因为它们来自浏览器;下行项是 Host 方法产出的值,原样透传。
50
+
51
+ `@Remote` 选项仅包含 `name`、`mode` 与下文所述的 `resources` 读取函数;输入策略由消费方选择,而非方法注解。`method.input(options?: TypertRemoteInputOptions)` 返回缓存的 `TypertRemoteValue<Input>`,唯一选项是 `strict?: boolean`。`method.input()` 与 `{ strict: false }` 保留普通 Remote 处理方式:已声明对象接受并剥离未知字段。`{ strict: true }` 递归拒绝未声明字段,而显式 JSON 值和字典键保持开放,字典值仍按声明校验。两种具名参数封装都拒绝额外参数。选择一个视图(`const input = method.input({ strict: true })`),即可通过 `input.create().parse(raw)` 与 `input.jsonSchema` 取得匹配的解析和投影;这不会改变 Host 分发。[生成器](<../generator/README.zh.md#ordinary-remote-metadata>)负责发布及配套 codec。
52
+
53
+ 普通 `./remote` 产物将 `TYPERT_REMOTE` 导出为 `TypertRemoteInfo`,它扩展可挂载的 `TypertRemoteContribution`。类型化的 `methods[endpoint]` 条目就是 `descriptors` 中同一个直接方法描述符对象,并带有派生的 `input(options?)` 和 `invoke(remote, input, signal?)`。参数文档位于 `parameters[].description`;`result` 是唯一的结果 codec、解析器和可选投影。一元描述符省略 `mode`;流使用 `'stream'`。仅使用 Context 接收器的方法保留在 `descriptors` 和作用域声明中,不进入 `methods`。
54
+
55
+ 已发布的纯入口 `@deepseek-ai/dsh-typert-protocol/remote-info` 提供 `createTypertRemoteInfo(spec, z)`,返回类实例而非构造函数。构造时只保留静态 spec 和调用方传入的 Zod 运行时;首次读取 `descriptors` 或 `methods` 时才构建并缓存两者共享的索引。每种输入策略的视图、解析器及可选 JSON Schema 投影都独立惰性求值并缓存;`method.input().create()` 请求普通解析器。原始 spec 要求 Zod factory,而面向消费方的 `TypertSchema<T>` 仅保证 `parse(value: unknown): T`。JSON Schema 来自生成器投影,而非运行时 `z.toJSONSchema()`。纯叶入口没有运行时导入;Zod 由生成产物提供。读取元数据和 schema 绝不发起网络调用;根入口也重新导出该 factory,但会导入 Cordis。
56
+
57
+ 调用器接收调用方拥有、已挂载且无作用域的 Client Remote,并在调用时解析其命名空间方法。它保留命名空间接收者、可选位置参数空位和 signal,不使用 async 包装,直接转发同一个 `Promise<RemoteResult<DecodedOutput>>` 或完整 `RemoteStreamHandle<Output, Uplink>`。`DecodedOutput` 默认采用原始输出类型;生成器仅为含字节的一元方法指定 `RemoteDecoded<Output>`,避免递归重映射普通 JSON 结果;`result.create().parse()` 保留原始输出类型,不会规范化共享字节缓冲区。任一输入变体或结果不支持 JSON Schema 时,文档与解析能力仍然保留;各变体的 `jsonSchemaError` 说明其投影缺失的原因。Registry 读取方使用同一纯入口提供的 `isTypertRemoteMethod(descriptor)`,将基础描述符收窄为这些直接调用能力。
58
+
59
+ ### 识别调用方并放行非 owner 调用方
60
+
61
+ `PeerScope.principal` 是进程内 Host 代码创建 Peer 时固定的调用方身份;任何 wire 字段都无法设置它,`PeerScope.signal` 则在 Peer scope 结束时中止。`PrincipalMap` 可合并扩展:每个驱动方声明一个 JSON 条目,带唯一的字面量 `kind` 且不含凭据,因为 principal 可能持久化到会话日志中;内置条目是 `owner: { kind: 'owner' }`。在持久输出上记录调用方的 Remote 方法调用 `principalOf(this.ctx.invocation)`:调用方不是 owner 时它返回该 principal,对 owner 以及在 Remote 调用之外返回 `undefined`;方法只在其有定义时写入该字段。
62
+
63
+ 每个 Remote 方法都受访问控制:由 [`dsh-api-remote-access`](../../api/remote-access/README.zh.md) 判定非 owner 调用方能否在该调用触及的资源上调用它。`TypertResourceMap` 与 `TypertLookupMap` 并列、可合并扩展:拥有身份类型的包以 `TypertResource<Id>` 声明自己的资源种类(`dsh-session` 声明 `session: TypertResource<SessionId>`,`dsh-workspace` 声明 `workspace: TypertResource<WorkspaceId>`)。`RemoteAccessResource` 要么是内置、无身份的 `{ kind: 'global' }`,要么是某个已声明种类加其身份;资源之间相互独立,`global` 上的访问不蕴含任何会话上的访问。运行时由拥有该种类的领域经 `typert.resources.register(kind, { field })` 登记默认 wire 字段,该登记拒绝 `global`;lookup provider 与 Host Context adapter 经 `resource` 声明其 wire 身份所属的种类,因此带 Agent 查找参数或 Agent 作用域接收器的方法无需选项即携带 `session` 资源。`@Remote` 与 `@RemoteScope` 的 `resources` 选项把种类映射到一个以方法 wire 参数为自身位置参数的读取函数;这些参数的类型从被装饰的方法推断,查找参数映射为其 wire 类型,因此 `@Remote({ name: 'page', resources: { session: request => request.address.sessionId } })` 可以读取默认规则读不到的字段。读取函数与默认规则都未给出资源的调用触及 `global`;它们必须给出方法访问的全部资源,因为访问控制从不检查被遗漏的资源。`TypertForwardableEventEntry` 中的转发 `emit` 条目可以声明 `resources` 读取函数,其类型由该事件自身的参数决定;未声明的条目触及 `global`。资源解析归 [API Gateway](../../api/gateway/README.zh.md);判定与拒绝原因归 [`dsh-api-remote-access`](../../api/remote-access/README.zh.md)。
50
64
 
51
65
  ### 把 Host 对象与 Context 关联到 wire identity
52
66
 
@@ -89,11 +103,15 @@ Host 装配以转发给消费方的 Cordis 事件扩展 `TypertRemoteEventSelect
89
103
 
90
104
  ### Remote 标记
91
105
 
92
- `@Remote` 与 `@RemoteScope` 调度一个初始化器,把方法名、可选导出名与调用模式追加到原型描述符;`remoteMethods(service)` 校验其版本,并返回与已存描述符分离、按声明顺序排列的快照,供 Gateway 的源码模式回退读取。标记要求名称为字符串的公开、非静态实例方法,同一方法上的冲突标记会被拒绝。
106
+ `@Remote` 与 `@RemoteScope` 调度一个初始化器,把方法名、可选导出名、调用模式与冻结的 `resources` 读取函数追加到原型描述符;`remoteMethods(service)` 校验其版本,并返回与已存描述符分离、按声明顺序排列的快照,供 Gateway 的源码模式回退读取。标记要求名称为字符串的公开、非静态实例方法,同一方法上的冲突标记会被拒绝。
107
+
108
+ 读取函数在创建装饰器时校验,并按每次装饰器应用冻结为一条记录;Gateway 按实现成员名从 `remoteMethods(service)` 读取它们,因此协议包的另一个已安装副本也能读取同一记录。
93
109
 
94
110
  ### 协议映射与描述符
95
111
 
96
- 可合并扩展的协议映射在类型系统中保留静态关联,运行时提供方则向 `ctx.typert` 注册解析;映射的名称与形状见 [`src/types.ts`](src/types.ts)。`InvocationDescriptor` 是注册表、Gateway 与 Client Remote 共同消费的共享运行时形式,涵盖直接与 Context 接收者、JSON 与查找参数、作用域投影、上行编解码器、取消与结果编解码器。
112
+ 可合并扩展的协议映射在类型系统中保留静态关联,运行时提供方则向 `ctx.typert` 注册解析;映射的名称与形状见 [`src/types.ts`](src/types.ts)。`InvocationDescriptor` 是注册表、Gateway 与 Client Remote 共同消费的共享运行时形式,涵盖直接与 Context 接收者、JSON 与查找参数、作用域投影、上行编解码器、取消、结果编解码器与文档。生成的 `TypertRemoteMethod` 对象直接扩展该描述符。
113
+
114
+ `RemoteInvocation.uplinkWindow` 可选地描述载体完整帧 inbox 的容量,以及紧凑 JSON 项之外的 UTF-8 封装开销。字段缺省表示载体没有编码帧 inbox 限额;消费者仍负责本地缓存与确认策略。
97
115
 
98
116
  ### Wire 标识文法
99
117
 
@@ -104,9 +122,12 @@ Host 装配以转发给消费方的 Cordis 事件扩展 `TypertRemoteEventSelect
104
122
  | 文件 | 职责 |
105
123
  |---|---|
106
124
  | [`src/index.ts`](src/index.ts) | 装饰器、Gateway 绑定、`remoteMethods`、段校验 |
125
+ | [`src/principal.ts`](src/principal.ts) | `principalOf`:Remote 调用生产者记录的调用方 principal |
126
+ | [`src/remote-access.ts`](src/remote-access.ts) | 基于 `TypertResourceMap` 的 `RemoteAccessResource`、`RemoteAccessResourceKind` 与 `RemoteAccessResourceOf` |
107
127
  | [`src/json-value.ts`](src/json-value.ts) | 各载体共享的无损 JSON 校验 `isRemoteJsonValue` 与 `isRemoteUplinkItem` |
108
128
  | [`src/remote-error.ts`](src/remote-error.ts) | `RemoteError` 与结构式识别函数 `remoteErrorOf` |
109
- | [`src/types.ts`](src/types.ts) | 协议映射、`RemoteErrorDetailsMap`、`RemoteResult`、`RemoteStream`、`RemoteStreamHandle`、`PeerScope`、`RemoteInvocation`、`InvocationDescriptor`、编解码器、提供方约定、注册表接口、`TypertClientRemote` |
129
+ | [`src/remote-info.ts`](src/remote-info.ts) | 纯惰性 info factory 与直接方法类型守卫 |
130
+ | [`src/types.ts`](src/types.ts) | 协议映射、`RemoteErrorDetailsMap`、`RemoteResult`、`RemoteStream`、`RemoteStreamHandle`、`PrincipalMap`、`Principal`、`PeerScope`、`RemoteInvocation`、`InvocationDescriptor`、编解码器、提供方约定、注册表接口、`TypertClientRemote` |
110
131
 
111
132
  </details>
112
133
 
@@ -141,7 +162,7 @@ Host 装配以转发给消费方的 Cordis 事件扩展 `TypertRemoteEventSelect
141
162
 
142
163
  这些限制说明声明能表示什么;它们是当前包约束,不是任务积压。
143
164
 
144
- - **装饰器标记是最小化的**——标记只包含方法名与直接调用或 Context 调用模式;参数、结果、查找与 schema 反射需要 Typert 构建流水线。
165
+ - **装饰器标记是最小化的**——标记包含方法名、可选的导出别名、流模式、资源读取函数,以及直接或 Context 接收器选择;参数、结果、查找、schema 与文档元数据需要 Typert 构建流水线。
145
166
  - **Remote 签名受限**——装饰器只接受具有字符串名称的公开、非静态实例方法,源码模式执行无法表示重载、解构、默认参数或剩余参数签名。
146
167
 
147
168
  <a id="dev-note"></a>
package/lib/index.js CHANGED
@@ -116,6 +116,186 @@ function visitJsonValue(value, ancestors) {
116
116
  }
117
117
  }
118
118
  //#endregion
119
+ //#region lib/types/remote-info.js
120
+ /** Lazy assembly of generated Remote definitions using caller-supplied Zod; no runtime imports. */
121
+ /**
122
+ * Retain one generated definition per endpoint without constructing parsers or projections.
123
+ * The class instance builds its shared descriptor/method index on first access;
124
+ * input schemas and JSON Schema projections are cached only when requested.
125
+ * @param spec - compiler-produced method facts and Zod factories.
126
+ * @param zod - the generated artifact's Zod runtime for named-input parsers.
127
+ * @returns a lazy, mountable info instance with no registration or transport effects.
128
+ */
129
+ function createTypertRemoteInfo(spec, zod) {
130
+ return new RemoteInfo(spec, zod);
131
+ }
132
+ var RemoteInfo = class {
133
+ spec;
134
+ zod;
135
+ index;
136
+ constructor(spec, zod) {
137
+ this.spec = spec;
138
+ this.zod = zod;
139
+ }
140
+ get package() {
141
+ return this.spec.package;
142
+ }
143
+ get descriptors() {
144
+ return this.resolve().descriptors;
145
+ }
146
+ get methods() {
147
+ return this.resolve().methods;
148
+ }
149
+ resolve() {
150
+ if (this.index !== void 0) return this.index;
151
+ const zod = this.zod;
152
+ const descriptors = [];
153
+ const methods = Object.create(null);
154
+ for (const [endpoint, definition] of Object.entries(this.spec.methods)) {
155
+ const separator = endpoint.indexOf("/");
156
+ const namespace = endpoint.slice(0, separator);
157
+ const method = endpoint.slice(separator + 1);
158
+ const parameters = definition.parameters.map((parameter) => ({
159
+ ...parameter,
160
+ wire: parameter.wire ?? parameter.name
161
+ }));
162
+ const descriptor = {
163
+ ...definition,
164
+ id: `${this.spec.package}#${endpoint}`,
165
+ namespace,
166
+ method,
167
+ parameters,
168
+ result: definition.result.description === void 0 ? definition.result : new ResultValue(definition.result)
169
+ };
170
+ if (definition.invocation.kind === "direct") {
171
+ const inputs = [];
172
+ methods[endpoint] = Object.assign(descriptor, {
173
+ input(options) {
174
+ const strict = options?.strict === true;
175
+ return inputs[strict ? 1 : 0] ??= new NamedInput(parameters, strict, zod);
176
+ },
177
+ invoke(remote, input, signal) {
178
+ const named = input;
179
+ const values = parameters.map((parameter) => named[parameter.wire]);
180
+ if (definition.cancellation !== void 0) values.push(signal);
181
+ const receiver = Reflect.get(remote, namespace);
182
+ return Reflect.get(receiver, method).apply(receiver, values);
183
+ }
184
+ });
185
+ }
186
+ descriptors.push(descriptor);
187
+ }
188
+ return this.index = {
189
+ descriptors,
190
+ methods
191
+ };
192
+ }
193
+ };
194
+ /**
195
+ * Narrow a Registry descriptor to its generated direct-method capabilities.
196
+ * @param descriptor - a live descriptor, including possible source fallback or Context-only receivers.
197
+ * @returns whether generated named input and direct invocation are available.
198
+ */
199
+ function isTypertRemoteMethod(descriptor) {
200
+ return descriptor.invocation.kind === "direct" && descriptor.result.mode === "strict" && "input" in descriptor && typeof descriptor.input === "function" && "invoke" in descriptor && typeof descriptor.invoke === "function";
201
+ }
202
+ function optional(parameter) {
203
+ return parameter.source === "json" && parameter.acceptsUndefined === true;
204
+ }
205
+ var NamedInput = class {
206
+ parameters;
207
+ strict;
208
+ zod;
209
+ schema;
210
+ projection;
211
+ constructor(parameters, strict, zod) {
212
+ this.parameters = parameters;
213
+ this.strict = strict;
214
+ this.zod = zod;
215
+ }
216
+ codec(parameter) {
217
+ return this.strict ? parameter.strictCodec : parameter.codec;
218
+ }
219
+ create = () => this.schema ??= this.zod.strictObject(Object.fromEntries(this.parameters.map((parameter) => {
220
+ const value = this.codec(parameter).create();
221
+ return [parameter.wire, optional(parameter) ? value.optional() : value.nonoptional()];
222
+ })));
223
+ get jsonSchema() {
224
+ return this.project().jsonSchema;
225
+ }
226
+ get jsonSchemaError() {
227
+ return this.project().jsonSchemaError;
228
+ }
229
+ project() {
230
+ if (this.projection !== void 0) return this.projection;
231
+ const unavailable = this.parameters.find((parameter) => this.codec(parameter).jsonSchema === void 0);
232
+ if (unavailable !== void 0) return this.projection = { jsonSchemaError: this.codec(unavailable).jsonSchemaError ?? `No JSON Schema for parameter ${unavailable.wire}` };
233
+ return this.projection = { jsonSchema: {
234
+ type: "object",
235
+ properties: Object.fromEntries(this.parameters.map((parameter) => [parameter.wire, {
236
+ ...this.codec(parameter).jsonSchema,
237
+ ...parameter.description === void 0 ? {} : { description: parameter.description }
238
+ }])),
239
+ required: this.parameters.filter((parameter) => !optional(parameter)).map((parameter) => parameter.wire),
240
+ additionalProperties: false
241
+ } };
242
+ }
243
+ };
244
+ var ResultValue = class {
245
+ spec;
246
+ projection;
247
+ constructor(spec) {
248
+ this.spec = spec;
249
+ }
250
+ get mode() {
251
+ return this.spec.mode;
252
+ }
253
+ get typeSymbol() {
254
+ return this.spec.typeSymbol;
255
+ }
256
+ get create() {
257
+ return this.spec.create;
258
+ }
259
+ get decode() {
260
+ return this.spec.decode;
261
+ }
262
+ get encode() {
263
+ return this.spec.encode;
264
+ }
265
+ get description() {
266
+ return this.spec.description;
267
+ }
268
+ get jsonSchemaError() {
269
+ return this.spec.jsonSchemaError;
270
+ }
271
+ get jsonSchema() {
272
+ if (this.projection === void 0) {
273
+ const schema = this.spec.jsonSchema;
274
+ this.projection = { value: schema === void 0 ? void 0 : {
275
+ ...schema,
276
+ description: this.spec.description
277
+ } };
278
+ }
279
+ return this.projection.value;
280
+ }
281
+ };
282
+ //#endregion
283
+ //#region lib/types/principal.js
284
+ /**
285
+ * Reads the non-owner caller of the current Remote call.
286
+ * @module @deepseek-ai/dsh-typert-protocol/principal
287
+ */
288
+ /**
289
+ * The identity a Remote-call producer records on durable output.
290
+ * @param invocation - `ctx.invocation`; `undefined` outside a Remote call.
291
+ * @returns the caller's principal when it is not the owner; `undefined` for the
292
+ * owner and for trusted in-process code outside a Remote call.
293
+ */
294
+ function principalOf(invocation) {
295
+ const principal = invocation?.peer.principal;
296
+ return principal?.kind === "owner" ? void 0 : principal;
297
+ }
298
+ //#endregion
119
299
  //#region lib/types/index.js
120
300
  /**
121
301
  * Remote decorators and explicit Gateway bindings backed by versioned
@@ -140,7 +320,7 @@ const REMOTE_METHOD_DESCRIPTOR = "@deepseek-ai/dsh-typert-protocol/remote-method
140
320
  * a Host composition to read it.
141
321
  * @param service - owning Service instance, normally `this`.
142
322
  * @param serviceKey - exact Cordis service key.
143
- * @param options - optional distinct wire namespace.
323
+ * @param options - optional wire namespace.
144
324
  * @returns a frozen, inspectable binding with no compiler-injected metadata.
145
325
  */
146
326
  function bindTypertRemote(service, serviceKey, options = {}) {
@@ -148,7 +328,7 @@ function bindTypertRemote(service, serviceKey, options = {}) {
148
328
  const namespace = options.namespace ?? serviceKey;
149
329
  validateName("namespace", namespace);
150
330
  const ctx = Reflect.get(service, "ctx");
151
- if (ctx instanceof Context) provideInvocationAccessor(ctx);
331
+ if (Context.is(ctx)) provideInvocationAccessor(ctx);
152
332
  return Object.freeze({
153
333
  service,
154
334
  serviceKey,
@@ -163,7 +343,7 @@ var TypertRemoteService = class extends Service {
163
343
  * Register the Service and bind the same key to Typert Gateway.
164
344
  * @param ctx - owning Cordis Context.
165
345
  * @param serviceKey - exact Cordis service key and default wire namespace.
166
- * @param options - optional distinct wire namespace.
346
+ * @param options - optional wire namespace.
167
347
  */
168
348
  constructor(ctx, serviceKey, options = {}) {
169
349
  super(ctx, serviceKey);
@@ -183,36 +363,57 @@ function provideInvocationAccessor(ctx) {
183
363
  function Remote(methodExportOrOptions, context) {
184
364
  if (typeof methodExportOrOptions === "string") {
185
365
  validateName("Remote export name", methodExportOrOptions);
186
- return remoteDecorator({ kind: "direct" }, void 0, methodExportOrOptions);
187
- }
188
- if (typeof methodExportOrOptions === "object") {
189
- if (remoteOptionMode(methodExportOrOptions) !== "stream" || Reflect.ownKeys(methodExportOrOptions).length !== 1) throw new TypeError("typert-protocol: Remote options must contain exactly mode: \"stream\"");
190
- return remoteDecorator({ kind: "direct" }, "stream");
366
+ return remoteDecorator({ kind: "direct" }, { exportName: methodExportOrOptions });
191
367
  }
368
+ if (typeof methodExportOrOptions === "object") return remoteDecorator({ kind: "direct" }, markerOptions(methodExportOrOptions, REMOTE_OPTION_KEYS, "Remote options require name, mode: \"stream\", or resources"));
192
369
  if (context === void 0) throw new TypeError("typert-protocol: Remote decorator context is missing");
193
370
  addMarkerInitializer(context, { kind: "direct" });
194
371
  }
195
- function remoteOptionMode(options) {
196
- return Reflect.get(options, "mode");
372
+ const REMOTE_OPTION_KEYS = new Set([
373
+ "name",
374
+ "mode",
375
+ "resources"
376
+ ]);
377
+ const REMOTE_SCOPE_OPTION_KEYS = new Set(["name", "resources"]);
378
+ /** Validate one decorator options object against its accepted keys and select the marker fields. */
379
+ function markerOptions(options, accepted, requirement) {
380
+ const keys = Reflect.ownKeys(options);
381
+ if (keys.length === 0 || keys.some((key) => typeof key !== "string" || !accepted.has(key)) || Object.hasOwn(options, "mode") && options.mode !== "stream") throw new TypeError(`typert-protocol: ${requirement}`);
382
+ const { mode, name, resources } = options;
383
+ if (name !== void 0) validateName("Remote export name", name);
384
+ return {
385
+ ...mode === void 0 ? {} : { mode },
386
+ ...name === void 0 ? {} : { exportName: name },
387
+ ...resources === void 0 ? {} : { resources: resourceReaders(resources) }
388
+ };
389
+ }
390
+ /** Freeze declared readers into the marker record; a misdeclared reader fails at decoration. */
391
+ function resourceReaders(resources) {
392
+ const readers = {};
393
+ for (const [kind, reader] of Object.entries(resources)) {
394
+ if (typeof reader !== "function") throw new TypeError(`typert-protocol: Remote resource reader "${kind}" must be a function`);
395
+ readers[kind] = reader;
396
+ }
397
+ if (Object.keys(readers).length === 0) throw new TypeError("typert-protocol: Remote resources require at least one reader");
398
+ return Object.freeze(readers);
197
399
  }
198
- function remoteDecorator(invocation, mode, exportName) {
400
+ function remoteDecorator(invocation, options = {}) {
199
401
  return function(_method, context) {
200
- addMarkerInitializer(context, invocation, mode, exportName);
402
+ addMarkerInitializer(context, invocation, options);
201
403
  };
202
404
  }
203
- /**
204
- * Create a decorator for a method resolved from one Remote Scope.
205
- * @param key - scope key declared through the Context map.
206
- * @param exportName - optional Remote export name; defaults to the method name.
207
- * @returns a standard method decorator that records a versioned prototype descriptor.
208
- */
209
- function RemoteScope(key, exportName) {
405
+ function RemoteScope(key, option) {
210
406
  validateName("Scope key", key);
211
- if (exportName !== void 0) validateName("Remote export name", exportName);
212
- return remoteDecorator({
407
+ const invocation = {
213
408
  kind: "context",
214
409
  context: key
215
- }, void 0, exportName);
410
+ };
411
+ if (option === void 0) return remoteDecorator(invocation);
412
+ if (typeof option === "string") {
413
+ validateName("Remote export name", option);
414
+ return remoteDecorator(invocation, { exportName: option });
415
+ }
416
+ return remoteDecorator(invocation, markerOptions(option, REMOTE_SCOPE_OPTION_KEYS, "RemoteScope options require name or resources"));
216
417
  }
217
418
  /**
218
419
  * Read Remote markers attached to a live Service's class prototype.
@@ -236,26 +437,27 @@ function readRemoteMethodDescriptor(prototype) {
236
437
  if (!Array.isArray(methods)) throw new TypeError("typert-protocol: Remote method descriptor methods must be an array");
237
438
  return descriptor;
238
439
  }
239
- function addMarkerInitializer(context, invocation, mode, exportName) {
440
+ function addMarkerInitializer(context, invocation, options = {}) {
240
441
  if (context.private || context.static || typeof context.name !== "string") throw new TypeError("typert-protocol: Remote decorators require a public instance method with a string name");
241
442
  const method = context.name;
242
443
  context.addInitializer(function() {
243
444
  const prototype = Object.getPrototypeOf(this);
244
445
  if (prototype === null) throw new TypeError(`typert-protocol: cannot mark Remote method "${method}" on an object without a prototype`);
245
- mark(prototype, method, invocation, mode, exportName);
446
+ mark(prototype, method, invocation, options);
246
447
  });
247
448
  }
248
- function mark(prototype, method, invocation, mode, exportName) {
449
+ function mark(prototype, method, invocation, { mode, exportName, resources }) {
249
450
  const descriptor = readRemoteMethodDescriptor(prototype);
250
451
  const marker = Object.freeze({
251
452
  method,
252
453
  ...exportName === void 0 || exportName === method ? {} : { exportName },
253
454
  ...mode === void 0 ? {} : { mode },
254
- invocation: Object.freeze(invocation)
455
+ invocation: Object.freeze(invocation),
456
+ ...resources === void 0 ? {} : { resources }
255
457
  });
256
458
  const current = descriptor?.methods.find((candidate) => candidate.method === method);
257
459
  if (current !== void 0) {
258
- if (current.exportName === marker.exportName && current.mode === marker.mode && sameInvocation(current.invocation, invocation)) return;
460
+ if (current.exportName === marker.exportName && current.mode === marker.mode && current.resources === marker.resources && sameInvocation(current.invocation, invocation)) return;
259
461
  throw new Error(`typert-protocol: Remote method "${method}" has conflicting invocation markers`);
260
462
  }
261
463
  Object.defineProperty(prototype, REMOTE_METHOD_DESCRIPTOR, {
@@ -275,4 +477,4 @@ function validateName(subject, value) {
275
477
  if (!isTypertRemoteSegment(value)) throw new TypeError(`typert-protocol: ${subject} must contain only RPC endpoint segment characters`);
276
478
  }
277
479
  //#endregion
278
- export { Remote, RemoteError, RemoteScope, TYPERT_OWNED_VALUE, TypertRemoteService, bindTypertRemote, isRemoteJsonValue, isRemoteUplinkItem, isTypertOwnedValue, isTypertRemoteSegment, remoteErrorOf, remoteMethods, typertOwnedValue };
480
+ export { Remote, RemoteError, RemoteScope, TYPERT_OWNED_VALUE, TypertRemoteService, bindTypertRemote, createTypertRemoteInfo, isRemoteJsonValue, isRemoteUplinkItem, isTypertOwnedValue, isTypertRemoteMethod, isTypertRemoteSegment, principalOf, remoteErrorOf, remoteMethods, typertOwnedValue };