@astrale-os/sdk 0.1.10 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/LICENSE +202 -0
  2. package/dist/auth/check.d.ts +1 -2
  3. package/dist/auth/check.d.ts.map +1 -1
  4. package/dist/auth/check.js +2 -6
  5. package/dist/auth/check.js.map +1 -1
  6. package/dist/auth/index.d.ts +3 -1
  7. package/dist/auth/index.d.ts.map +1 -1
  8. package/dist/auth/index.js +2 -1
  9. package/dist/auth/index.js.map +1 -1
  10. package/dist/auth/issuer-mint.d.ts +53 -0
  11. package/dist/auth/issuer-mint.d.ts.map +1 -0
  12. package/dist/auth/issuer-mint.js +35 -0
  13. package/dist/auth/issuer-mint.js.map +1 -0
  14. package/dist/auth/kernel-client.d.ts +8 -2
  15. package/dist/auth/kernel-client.d.ts.map +1 -1
  16. package/dist/auth/kernel-client.js +22 -46
  17. package/dist/auth/kernel-client.js.map +1 -1
  18. package/dist/cli/dotenv.d.ts +8 -0
  19. package/dist/cli/dotenv.d.ts.map +1 -1
  20. package/dist/cli/dotenv.js +18 -0
  21. package/dist/cli/dotenv.js.map +1 -1
  22. package/dist/cli/index.d.ts +1 -1
  23. package/dist/cli/index.d.ts.map +1 -1
  24. package/dist/cli/index.js +1 -1
  25. package/dist/cli/index.js.map +1 -1
  26. package/dist/cli/log.d.ts +11 -0
  27. package/dist/cli/log.d.ts.map +1 -0
  28. package/dist/cli/log.js +17 -0
  29. package/dist/cli/log.js.map +1 -0
  30. package/dist/cli/publish.d.ts +35 -0
  31. package/dist/cli/publish.d.ts.map +1 -0
  32. package/dist/cli/publish.js +271 -0
  33. package/dist/cli/publish.js.map +1 -0
  34. package/dist/cli/run.d.ts +17 -2
  35. package/dist/cli/run.d.ts.map +1 -1
  36. package/dist/cli/run.js +102 -111
  37. package/dist/cli/run.js.map +1 -1
  38. package/dist/cli/spec.d.ts.map +1 -1
  39. package/dist/cli/spec.js +1 -0
  40. package/dist/cli/spec.js.map +1 -1
  41. package/dist/config/adapter.d.ts +12 -11
  42. package/dist/config/adapter.d.ts.map +1 -1
  43. package/dist/config/adapter.js.map +1 -1
  44. package/dist/config/define-domain.d.ts +88 -27
  45. package/dist/config/define-domain.d.ts.map +1 -1
  46. package/dist/config/define-domain.js +79 -33
  47. package/dist/config/define-domain.js.map +1 -1
  48. package/dist/config/deploy.d.ts +1 -1
  49. package/dist/config/deploy.js +1 -1
  50. package/dist/config/index.d.ts +1 -1
  51. package/dist/config/index.d.ts.map +1 -1
  52. package/dist/define/remote-function.d.ts +32 -17
  53. package/dist/define/remote-function.d.ts.map +1 -1
  54. package/dist/define/remote-function.js +10 -9
  55. package/dist/define/remote-function.js.map +1 -1
  56. package/dist/define/view.d.ts +8 -6
  57. package/dist/define/view.d.ts.map +1 -1
  58. package/dist/define/view.js +8 -6
  59. package/dist/define/view.js.map +1 -1
  60. package/dist/deploy/check.d.ts +6 -6
  61. package/dist/deploy/check.d.ts.map +1 -1
  62. package/dist/deploy/check.js +1 -1
  63. package/dist/deploy/check.js.map +1 -1
  64. package/dist/deploy/index.d.ts +0 -1
  65. package/dist/deploy/index.d.ts.map +1 -1
  66. package/dist/deploy/index.js +0 -1
  67. package/dist/deploy/index.js.map +1 -1
  68. package/dist/deploy/meta.d.ts +11 -0
  69. package/dist/deploy/meta.d.ts.map +1 -1
  70. package/dist/deploy/meta.js +23 -0
  71. package/dist/deploy/meta.js.map +1 -1
  72. package/dist/dispatch/dispatcher.d.ts +17 -4
  73. package/dist/dispatch/dispatcher.d.ts.map +1 -1
  74. package/dist/dispatch/dispatcher.js +15 -3
  75. package/dist/dispatch/dispatcher.js.map +1 -1
  76. package/dist/dispatch/execute.d.ts +6 -2
  77. package/dist/dispatch/execute.d.ts.map +1 -1
  78. package/dist/dispatch/execute.js +1 -0
  79. package/dist/dispatch/execute.js.map +1 -1
  80. package/dist/dispatch/identity.d.ts +12 -10
  81. package/dist/dispatch/identity.d.ts.map +1 -1
  82. package/dist/dispatch/identity.js +10 -15
  83. package/dist/dispatch/identity.js.map +1 -1
  84. package/dist/dispatch/index.d.ts +1 -1
  85. package/dist/dispatch/index.d.ts.map +1 -1
  86. package/dist/dispatch/index.js +1 -1
  87. package/dist/dispatch/index.js.map +1 -1
  88. package/dist/dispatch/self.d.ts +25 -37
  89. package/dist/dispatch/self.d.ts.map +1 -1
  90. package/dist/dispatch/self.js +60 -30
  91. package/dist/dispatch/self.js.map +1 -1
  92. package/dist/domain/binding.d.ts +18 -0
  93. package/dist/domain/binding.d.ts.map +1 -0
  94. package/dist/domain/binding.js +29 -0
  95. package/dist/domain/binding.js.map +1 -0
  96. package/dist/domain/build-spec.d.ts.map +1 -1
  97. package/dist/domain/build-spec.js +22 -7
  98. package/dist/domain/build-spec.js.map +1 -1
  99. package/dist/domain/define.d.ts +28 -43
  100. package/dist/domain/define.d.ts.map +1 -1
  101. package/dist/domain/define.js +23 -60
  102. package/dist/domain/define.js.map +1 -1
  103. package/dist/domain/extend-functions.d.ts +48 -0
  104. package/dist/domain/extend-functions.d.ts.map +1 -0
  105. package/dist/domain/extend-functions.js +84 -0
  106. package/dist/domain/extend-functions.js.map +1 -0
  107. package/dist/domain/extend-views.d.ts +45 -0
  108. package/dist/domain/extend-views.d.ts.map +1 -0
  109. package/dist/domain/extend-views.js +116 -0
  110. package/dist/domain/extend-views.js.map +1 -0
  111. package/dist/index.d.ts +8 -4
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +13 -5
  114. package/dist/index.js.map +1 -1
  115. package/dist/method/class.d.ts +2 -2
  116. package/dist/method/class.d.ts.map +1 -1
  117. package/dist/method/class.js.map +1 -1
  118. package/dist/method/context.d.ts +18 -3
  119. package/dist/method/context.d.ts.map +1 -1
  120. package/dist/method/index.d.ts +2 -2
  121. package/dist/method/index.d.ts.map +1 -1
  122. package/dist/method/index.js.map +1 -1
  123. package/dist/method/single.d.ts +25 -38
  124. package/dist/method/single.d.ts.map +1 -1
  125. package/dist/method/single.js +1 -11
  126. package/dist/method/single.js.map +1 -1
  127. package/dist/server/auxiliary-routes.d.ts.map +1 -1
  128. package/dist/server/auxiliary-routes.js +14 -3
  129. package/dist/server/auxiliary-routes.js.map +1 -1
  130. package/dist/server/create.d.ts +2 -15
  131. package/dist/server/create.d.ts.map +1 -1
  132. package/dist/server/create.js +15 -51
  133. package/dist/server/create.js.map +1 -1
  134. package/dist/server/domain-entry.d.ts +10 -0
  135. package/dist/server/domain-entry.d.ts.map +1 -1
  136. package/dist/server/domain-entry.js +2 -0
  137. package/dist/server/domain-entry.js.map +1 -1
  138. package/dist/server/index.d.ts +0 -1
  139. package/dist/server/index.d.ts.map +1 -1
  140. package/dist/server/index.js +0 -1
  141. package/dist/server/index.js.map +1 -1
  142. package/dist/server/worker-entry.d.ts +3 -3
  143. package/dist/server/worker-entry.js +3 -3
  144. package/package.json +10 -11
  145. package/src/auth/check.ts +2 -6
  146. package/src/auth/index.ts +3 -1
  147. package/src/auth/issuer-mint.ts +75 -0
  148. package/src/auth/kernel-client.ts +31 -63
  149. package/src/cli/dotenv.ts +20 -0
  150. package/src/cli/index.ts +1 -1
  151. package/src/cli/log.ts +18 -0
  152. package/src/cli/publish.ts +341 -0
  153. package/src/cli/run.ts +109 -125
  154. package/src/cli/spec.ts +1 -0
  155. package/src/config/adapter.ts +9 -11
  156. package/src/config/define-domain.ts +184 -53
  157. package/src/config/deploy.ts +1 -1
  158. package/src/config/index.ts +1 -1
  159. package/src/define/remote-function.ts +56 -21
  160. package/src/define/view.ts +8 -6
  161. package/src/deploy/check.ts +7 -7
  162. package/src/deploy/index.ts +0 -1
  163. package/src/deploy/meta.ts +25 -0
  164. package/src/dispatch/dispatcher.ts +32 -9
  165. package/src/dispatch/execute.ts +7 -2
  166. package/src/dispatch/identity.ts +17 -22
  167. package/src/dispatch/index.ts +1 -1
  168. package/src/dispatch/self.ts +117 -41
  169. package/src/domain/binding.ts +37 -0
  170. package/src/domain/build-spec.ts +32 -7
  171. package/src/domain/define.ts +71 -78
  172. package/src/domain/extend-functions.ts +106 -0
  173. package/src/domain/extend-views.ts +151 -0
  174. package/src/index.ts +36 -7
  175. package/src/method/class.ts +11 -7
  176. package/src/method/context.ts +19 -3
  177. package/src/method/index.ts +8 -2
  178. package/src/method/single.ts +35 -42
  179. package/src/server/auxiliary-routes.ts +14 -3
  180. package/src/server/create.ts +15 -51
  181. package/src/server/domain-entry.ts +12 -0
  182. package/src/server/index.ts +0 -1
  183. package/src/server/worker-entry.ts +3 -3
  184. package/src/deploy/hash-spec.ts +0 -31
  185. package/src/domain/extend-core.ts +0 -301
  186. package/src/server/worker-meta.ts +0 -25
@@ -11,13 +11,18 @@
11
11
 
12
12
  import type { AuthedKernelAPI, DispatchResult, KernelAPI } from '@astrale-os/kernel-api'
13
13
  import type { AuthPolicy } from '@astrale-os/kernel-api/routed'
14
- import type { CredentialInput, Path } from '@astrale-os/kernel-core'
14
+ import type { NodeBinder } from '@astrale-os/kernel-client/schema'
15
+ import type { CredentialInput } from '@astrale-os/kernel-core'
15
16
  import type { BoundMethod, CompiledDomain } from '@astrale-os/kernel-core/domain'
17
+ import type { Schema } from '@astrale-os/kernel-dsl'
18
+
19
+ import { createNodeBinder } from '@astrale-os/kernel-client/schema'
16
20
 
17
21
  import type { RemoteIdentityConfig } from '../auth/identity'
18
22
  import type { AnyRemoteHandler } from '../method/single'
19
23
  import type { MethodIndex } from './resolve'
20
24
 
25
+ import { makeIssuerMint } from '../auth/issuer-mint'
21
26
  import { resolveInboundAuth } from '../auth/resolve'
22
27
  import { runAuthorize } from './authorize'
23
28
  import { makeCallRemote } from './call-remote'
@@ -25,9 +30,11 @@ import { MethodNotFoundError, SdkResultValidationError, SdkValidationError } fro
25
30
  import { executeHandler } from './execute'
26
31
  import { buildIdentityMap } from './identity'
27
32
  import { resolveMethod } from './resolve'
28
- import { resolveSelf, withNode, type ParsedSelf } from './self'
33
+ import { resolveSelf, withBoundNode, withNode, type ParsedSelf } from './self'
29
34
  import { validateParams, validateResult } from './validate'
30
35
 
36
+ type PathInput = string | { raw: string }
37
+
31
38
  export type SdkDispatcherConfig<TDeps> = {
32
39
  /** The compiled domain — source of the method layout and origin-addressed subs. */
33
40
  compiled: CompiledDomain
@@ -46,6 +53,12 @@ export type SdkDispatcherConfig<TDeps> = {
46
53
  issuer: string
47
54
  /** The worker's serving URL (`config.url`) — exposed to handlers as `ctx.url`. */
48
55
  url: string
56
+ /**
57
+ * The domain schema. When present, non-static handlers receive the rich,
58
+ * schema-typed `self` ({@link withBoundNode}: `self.node()` → `BoundNode<D>`,
59
+ * typed `self.call`); absent, they fall back to the untyped {@link withNode}.
60
+ */
61
+ schema?: Schema
49
62
  }
50
63
 
51
64
  export class SdkDispatcher<TDeps = unknown> implements KernelAPI {
@@ -55,6 +68,8 @@ export class SdkDispatcher<TDeps = unknown> implements KernelAPI {
55
68
  private readonly issuer: string
56
69
  private readonly url: string
57
70
  private readonly privateKey: JsonWebKey
71
+ /** Built once when a schema is supplied — drives the rich typed `self`. */
72
+ private readonly binder: NodeBinder<Schema> | null
58
73
 
59
74
  constructor(config: SdkDispatcherConfig<TDeps>) {
60
75
  this.methods = config.methods
@@ -62,6 +77,7 @@ export class SdkDispatcher<TDeps = unknown> implements KernelAPI {
62
77
  this.issuer = config.issuer
63
78
  this.url = config.url
64
79
  this.privateKey = config.privateKey
80
+ this.binder = config.schema ? createNodeBinder(config.schema) : null
65
81
  const methodList = Array.from(new Set(config.methods.values()))
66
82
  const identityMethods = methodList.filter((bound) => methodAuthPolicy(bound) !== 'public')
67
83
  this.identities =
@@ -71,7 +87,7 @@ export class SdkDispatcher<TDeps = unknown> implements KernelAPI {
71
87
  }
72
88
 
73
89
  async call(
74
- path: Path | string,
90
+ path: PathInput,
75
91
  credential: CredentialInput,
76
92
  params: unknown,
77
93
  opts?: { self?: string },
@@ -80,7 +96,7 @@ export class SdkDispatcher<TDeps = unknown> implements KernelAPI {
80
96
  }
81
97
 
82
98
  async stream(
83
- path: Path | string,
99
+ path: PathInput,
84
100
  credential: CredentialInput,
85
101
  params: unknown,
86
102
  opts?: { self?: string },
@@ -98,7 +114,7 @@ export class SdkDispatcher<TDeps = unknown> implements KernelAPI {
98
114
  * and hand it back to the kernel-api orchestrator.
99
115
  */
100
116
  async dispatch(
101
- path: Path | string,
117
+ path: PathInput,
102
118
  credential: CredentialInput,
103
119
  params: unknown,
104
120
  opts?: { self?: string },
@@ -117,12 +133,12 @@ export class SdkDispatcher<TDeps = unknown> implements KernelAPI {
117
133
  }
118
134
 
119
135
  private async run(
120
- path: Path | string,
136
+ path: PathInput,
121
137
  credential: CredentialInput,
122
138
  params: unknown,
123
139
  selfRef?: string,
124
140
  ): Promise<unknown> {
125
- const bound = resolveMethod(this.methods, path)
141
+ const bound = resolveMethod(this.methods, pathToString(path))
126
142
  if (!bound) throw new MethodNotFoundError(pathToString(path))
127
143
 
128
144
  const authPolicy: AuthPolicy = (bound.handler as { auth?: AuthPolicy }).auth ?? 'required'
@@ -177,7 +193,13 @@ export class SdkDispatcher<TDeps = unknown> implements KernelAPI {
177
193
  // Enrich the parsed self with the lazy `node()` accessor, bound to this
178
194
  // request's kernel (null when the auth policy yields none — `node()` then
179
195
  // rejects). Done here, not in `resolveSelf`, so the parse stays kernel-free.
180
- const self = parsedSelf ? withNode(parsedSelf, kernel) : undefined
196
+ // With a schema the handler gets the rich typed `self` keyed by its owner
197
+ // (class/interface); without one, the untyped fallback.
198
+ const self = parsedSelf
199
+ ? this.binder
200
+ ? withBoundNode(parsedSelf, kernel, this.binder, bound.owner)
201
+ : withNode(parsedSelf, kernel)
202
+ : undefined
181
203
  const ctx = {
182
204
  params: handlerParams,
183
205
  auth,
@@ -185,6 +207,7 @@ export class SdkDispatcher<TDeps = unknown> implements KernelAPI {
185
207
  deps: this.deps,
186
208
  url: this.url,
187
209
  kernel,
210
+ issuer: makeIssuerMint(fnIdentity),
188
211
  callRemote,
189
212
  }
190
213
 
@@ -213,7 +236,7 @@ function methodAuthPolicy(bound: BoundMethod<AnyRemoteHandler>): AuthPolicy {
213
236
  return (bound.handler as { auth?: AuthPolicy }).auth ?? 'required'
214
237
  }
215
238
 
216
- function pathToString(path: Path | string): string {
239
+ function pathToString(path: PathInput): string {
217
240
  return typeof path === 'string' ? path : path.raw
218
241
  }
219
242
 
@@ -11,8 +11,9 @@ import type { FnMap } from '@astrale-os/kernel-client'
11
11
  import type { BoundClientSessionView } from '@astrale-os/kernel-client/session'
12
12
  import type { AuthContext } from '@astrale-os/kernel-core'
13
13
 
14
+ import type { DomainIssuer } from '../auth/issuer-mint'
14
15
  import type { CallRemoteFn } from './call-remote'
15
- import type { SelfResult } from './self'
16
+ import type { SelfResult, TypedSelf } from './self'
16
17
 
17
18
  // oxlint-disable-next-line no-explicit-any
18
19
  type HandlerFn = (...args: any[]) => any
@@ -25,10 +26,13 @@ export type ExecuteParams = {
25
26
  ref?: string
26
27
  params: Record<string, unknown>
27
28
  auth: AuthContext | null
28
- self: SelfResult | undefined
29
+ /** Untyped fallback ({@link withNode}) or the rich typed self ({@link withBoundNode}). */
30
+ self: SelfResult | TypedSelf<unknown> | undefined
29
31
  deps: unknown
30
32
  url: string
31
33
  kernel: BoundClientSessionView<FnMap> | null
34
+ /** Mint seam for identities this domain issues (`ctx.issuer`). */
35
+ issuer: DomainIssuer
32
36
  callRemote: CallRemoteFn
33
37
  }
34
38
 
@@ -46,6 +50,7 @@ export async function executeHandler(ctx: ExecuteParams): Promise<unknown> {
46
50
  deps: ctx.deps,
47
51
  url: ctx.url,
48
52
  kernel: ctx.kernel,
53
+ issuer: ctx.issuer,
49
54
  callRemote: ctx.callRemote,
50
55
  })
51
56
  }
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Per-callable identity — dispatcher runtime + install-time wiring.
3
3
  *
4
- * Two flavors of callable get an identity per the install-time identity
5
- * binding: Methods on classes/interfaces (sub = MethodPath) and auto-
6
- * materialized core function nodes RemoteFunctions and Views (sub =
7
- * AbsolutePath of the node under `core/{functions,views}/<slug>`).
4
+ * Three flavors of callable get an identity per the install-time identity
5
+ * binding, each keyed by its mount-stable semantic sub: Methods on
6
+ * classes/interfaces (sub = MethodPath), standalone-function MEMBERS
7
+ * (sub = FunctionPath `/:<origin>:function.<slug>`), and view MEMBERS
8
+ * (sub = ViewPath `/:<origin>:view.<slug>`).
8
9
  *
9
10
  * At server startup the method dispatcher and the View / RemoteFunction
10
11
  * route mounter pre-compute, for every materialized callable, the
@@ -54,11 +55,12 @@ export type AuxBuckets<T> = {
54
55
  }
55
56
 
56
57
  /**
57
- * slug → `AbsolutePath.raw` for each auto-materialized aux node
58
- * (RemoteFunction / View). These nodes live at
59
- * `/<origin>/core/{functions,views}/<slug>` and have no class+method
60
- * decomposition that would justify a MethodPath their identity is their
61
- * graph position.
58
+ * slug → mount-stable semantic `sub` for each aux callable MEMBER node:
59
+ * - standalone-function members (`kind: 'function'`) FunctionPath;
60
+ * - view members (`kind: 'view'`) → ViewPath (plus any hand-authored/legacy
61
+ * core `View` nodes, `kind: 'core'` className `View`, keyed by their AbsolutePath).
62
+ * The value is `resolveCallables(...).sub` — the exact subject the kernel stamped on
63
+ * the node at install, so an outbound-signing worker matches its own function identity.
62
64
  */
63
65
  export type AuxIdentityPaths = AuxBuckets<string>
64
66
 
@@ -66,20 +68,13 @@ export function collectAuxIdentityPaths(compiled: CompiledDomain): AuxIdentityPa
66
68
  const views: Record<string, string> = {}
67
69
  const remoteFunctions: Record<string, string> = {}
68
70
  for (const c of resolveCallables(compiled)) {
69
- if (c.kind !== 'core' || !c.slug) continue
70
- if (c.className === 'View') {
71
- views[c.slug] = c.sub
72
- } else if (c.className === 'Function') {
73
- // Standalone callables (the former `RemoteFunction`) materialize as the
74
- // canonical kernel `Function` class.
71
+ if (!c.slug) continue
72
+ if (c.kind === 'function') {
75
73
  remoteFunctions[c.slug] = c.sub
76
- } else {
77
- throw new Error(
78
- `collectAuxIdentityPaths: unrecognised aux callable className "${c.className}" ` +
79
- `at ${c.ref}. If you've added a new auto-materialized Function class to extendCore, ` +
80
- `extend this collector + the kernel walker (resolveCallables) to handle it.`,
81
- )
74
+ } else if (c.kind === 'view' || (c.kind === 'core' && c.className === 'View')) {
75
+ views[c.slug] = c.sub
82
76
  }
77
+ // Methods (`kind: 'method'`) get their identity via `collectMethodPaths`.
83
78
  }
84
79
  return { views, remoteFunctions }
85
80
  }
@@ -88,7 +83,7 @@ export function collectAuxIdentityPaths(compiled: CompiledDomain): AuxIdentityPa
88
83
  * slug → `RemoteIdentityConfig` for each auto-materialized View /
89
84
  * RemoteFunction. Built once at server startup; consumed by
90
85
  * `mountAuxiliaryRoutes` so each handler signs outbound `kernel.call(...)`
91
- * with its own `sub` (the node's AbsolutePath). Mirrors `buildIdentityMap`.
86
+ * with its own `sub` (its FunctionPath/ViewPath). Mirrors `buildIdentityMap`.
92
87
  */
93
88
  export type AuxIdentityMap = AuxBuckets<RemoteIdentityConfig>
94
89
 
@@ -6,7 +6,7 @@ export {
6
6
  type ValidationResult,
7
7
  type ResultValidationResult,
8
8
  } from './validate'
9
- export { resolveSelf, type SelfResult } from './self'
9
+ export { resolveSelf, withBoundNode, type SelfResult, type TypedSelf } from './self'
10
10
  export { executeHandler, type ExecuteParams } from './execute'
11
11
  export { makeCallRemote, type CallRemoteFn, type KernelCaller } from './call-remote'
12
12
  export {
@@ -1,30 +1,26 @@
1
1
  /**
2
- * Self resolution pipeline step 3.
3
- *
4
- * `_self` is a path reference to the node the non-static method runs on.
5
- * `resolveSelf` is a thin PARSE: the caller's string becomes a `Path`, and
6
- * handlers build recursive paths as `${self.path}::method` (the `Path`'s
7
- * `toString` yields its raw form, so template literals work unchanged). It does
8
- * NOT touch the graph — the whole dispatch pipeline (resolve → authenticate →
9
- * validate → self → execute) reaches the handler without a single `Node::get`.
10
- *
11
- * `resolveSelf` receives the bare self target (`@<id>` or a tree path) — the
12
- * method ref travels in a separate dispatch channel, so `::method` never
13
- * reaches here. For the `@<id>` form the parsed `Path` is an `IdPath` which
14
- * already carries the node id; surfacing it on `SelfResult.id` saves every
15
- * worker handler from parsing `self.path.raw` by hand to recover the id of the
16
- * node it is already operating on.
17
- *
18
- * `withNode` enriches the parsed self with the lazy `node()` accessor — the one
19
- * place a handler can fetch its own node's full record (`{ id, class, props }`)
20
- * without hand-rolling a `::get`. Construction stays split from parsing so
21
- * `resolveSelf` remains pure and trivially testable; the dispatcher attaches the
22
- * accessor once it holds the request's kernel.
2
+ * Self resolution stays parse-only: `_self` becomes a Path/IdPath with no graph
3
+ * read. The dispatcher later attaches lazy node()/call helpers once it has a kernel.
23
4
  */
24
5
 
6
+ import type {
7
+ BindCtx,
8
+ BoundNode,
9
+ InstanceDispatch,
10
+ NodeBinder,
11
+ ProxyCaller,
12
+ TypeMethods,
13
+ TypeNames,
14
+ } from '@astrale-os/kernel-client/schema'
25
15
  import type { Node } from '@astrale-os/kernel-core/graph'
16
+ import type { Schema } from '@astrale-os/kernel-dsl'
26
17
 
27
- import { IdPath, Path, type NodeId } from '@astrale-os/kernel-core'
18
+ import {
19
+ createInstanceProxy,
20
+ defaultCaller,
21
+ extractMethodsByType,
22
+ } from '@astrale-os/kernel-client/schema'
23
+ import { IdPath, K, Path, type NodeId } from '@astrale-os/kernel-core'
28
24
  import { classPathSchema } from '@astrale-os/kernel-core/domain'
29
25
  import { absolutePathSchema } from '@astrale-os/kernel-core/tree'
30
26
  import { z } from 'zod'
@@ -43,32 +39,31 @@ export type ParsedSelf = {
43
39
 
44
40
  export type SelfResult = ParsedSelf & {
45
41
  /**
46
- * Lazily read THIS node's full record via one `::get` on the handler's
47
- * kernel. Memoized for the dispatch: repeated and concurrent calls share a
48
- * single in-flight fetch, and a failure clears the cache so it stays
49
- * retryable. Pass `{ reload: true }` to force a fresh read.
50
- *
51
- * The result is a point-in-time SNAPSHOT, not a live view — for
52
- * read-modify-write, treat it as the value as of the read (or `reload`).
53
- * `props` is returned structurally validated only, NOT typed against the
54
- * node's declared schema: the kernel does not hydrate declared props at
55
- * runtime, so a node-typed `self` would be unsound. Read the concrete backend
56
- * off `node.class` (a `ClassPath`) — that is the persisted discriminator.
42
+ * Lazy point-in-time self read. Memoized per dispatch; `{ reload: true }`
43
+ * forces a fresh `function.get`.
57
44
  */
58
45
  node(opts?: { reload?: boolean }): Promise<ResolvedSelfNode>
59
46
  }
60
47
 
48
+ /** Schema-typed self: short-keyed props and typed same-instance call proxy. */
49
+ export type TypedSelf<D, S extends Schema = Schema> = ParsedSelf & {
50
+ /**
51
+ * Fetch THIS node as a typed `BoundNode<D>`. Memoized exactly like
52
+ * {@link SelfResult.node} — one in-flight `function.get`, rejection-clearing,
53
+ * `{ reload: true }` refetches — so the zero-extra-graph-read dispatch
54
+ * invariant holds: nothing is read until called, then at most once.
55
+ */
56
+ node(opts?: { reload?: boolean }): Promise<BoundNode<D, S>>
57
+ /** Own instance methods — typed, address-only (`<selfPath>::method`), no read. */
58
+ readonly call: InstanceDispatch<D>
59
+ }
60
+
61
61
  export function resolveSelf(ref: string): ParsedSelf {
62
62
  const path = Path.parse(ref)
63
63
  return path instanceof IdPath ? { path, id: path.id } : { path }
64
64
  }
65
65
 
66
- /**
67
- * Attach the memoized `node()` accessor, binding it to the handler's kernel.
68
- * `kernel` is `null` for methods whose auth policy yields none (`public`, or an
69
- * unauthenticated `optional`) — there `node()` rejects with a clear error
70
- * rather than reading the graph unauthenticated.
71
- */
66
+ /** Attach memoized node(); unauthenticated contexts reject instead of reading. */
72
67
  export function withNode(parsed: ParsedSelf, kernel: Kernel | null): SelfResult {
73
68
  let cached: Promise<ResolvedSelfNode> | undefined
74
69
  const node = (opts?: { reload?: boolean }): Promise<ResolvedSelfNode> => {
@@ -84,6 +79,73 @@ export function withNode(parsed: ParsedSelf, kernel: Kernel | null): SelfResult
84
79
  return { ...parsed, node }
85
80
  }
86
81
 
82
+ const EMPTY_METHODS: TypeMethods = { kind: 'class', methods: new Set() }
83
+ const methodMapCache = new WeakMap<object, Map<string, TypeMethods>>()
84
+
85
+ /** The type's method allow-list + kind, computed once per binder. */
86
+ function methodsFor<S extends Schema>(binder: NodeBinder<S>, typeName: string): TypeMethods {
87
+ let map = methodMapCache.get(binder)
88
+ if (!map) {
89
+ map = extractMethodsByType(binder.schema)
90
+ methodMapCache.set(binder, map)
91
+ }
92
+ return map.get(typeName) ?? EMPTY_METHODS
93
+ }
94
+
95
+ function noKernelCaller(ref: string): ProxyCaller {
96
+ return () =>
97
+ Promise.reject(
98
+ new Error(
99
+ `self.call needs an authenticated kernel to dispatch on "${ref}", but this method ran ` +
100
+ `without one (its auth policy yields no kernel — e.g. 'public').`,
101
+ ),
102
+ )
103
+ }
104
+
105
+ /**
106
+ * Schema-typed self helper: lazy BoundNode plus address-only call proxy. With no
107
+ * kernel, both node() and call thunks fail loudly.
108
+ */
109
+ export function withBoundNode<S extends Schema>(
110
+ parsed: ParsedSelf,
111
+ kernel: Kernel | null,
112
+ binder: NodeBinder<S>,
113
+ typeName: string,
114
+ ): TypedSelf<unknown, S> {
115
+ const ctx: BindCtx = kernel
116
+ ? { caller: defaultCaller(kernel), read: kernel }
117
+ : { caller: null, read: null }
118
+
119
+ let cached: Promise<BoundNode<unknown, S>> | undefined
120
+ const node = (opts?: { reload?: boolean }): Promise<BoundNode<unknown, S>> => {
121
+ if (opts?.reload) cached = undefined
122
+ if (!cached) {
123
+ cached = fetchSelfNode(parsed.path, kernel)
124
+ .then(
125
+ (record) =>
126
+ binder.bind(typeName as TypeNames<S>, record, ctx, {
127
+ validate: 'warn',
128
+ }) as unknown as BoundNode<unknown, S>,
129
+ )
130
+ .catch((err: unknown) => {
131
+ cached = undefined // never cache a rejection — a later call may succeed
132
+ throw err
133
+ })
134
+ }
135
+ return cached
136
+ }
137
+
138
+ const info = methodsFor(binder, typeName)
139
+ const call = createInstanceProxy(
140
+ ctx.caller ?? noKernelCaller(parsed.path.raw),
141
+ parsed.path.raw,
142
+ info.methods,
143
+ info.kind === 'class' ? typeName : undefined,
144
+ ) as InstanceDispatch<unknown>
145
+
146
+ return { ...parsed, node, call }
147
+ }
148
+
87
149
  /**
88
150
  * Boundary schema for a node record off the wire. Structurally identical to
89
151
  * kernel-core's `nodeSchema`, re-declared here from its public coercers
@@ -108,9 +170,23 @@ async function fetchSelfNode(path: Path, kernel: Kernel | null): Promise<Resolve
108
170
  `method whose auth is 'required', or supply a credential.`,
109
171
  )
110
172
  }
111
- const record = nodeRecordSchema.parse(await kernel.call(`${path.raw}::get`, {}))
173
+ // function.get soft-masks missing/unreadable roots; empty page means no self node.
174
+ const result = (await kernel.call(K.$.f('get').path.domain.raw, {
175
+ roots: [path.raw],
176
+ depth: 0,
177
+ })) as {
178
+ nodes?: unknown[]
179
+ }
180
+ const first = result?.nodes?.[0]
181
+ if (first === undefined) {
182
+ throw new Error(
183
+ `self.node(): "${path.raw}" was not found or is not visible to the caller ` +
184
+ `(function.get returned no node).`,
185
+ )
186
+ }
187
+ const record = nodeRecordSchema.parse(first)
112
188
  if (record.id === undefined) {
113
- throw new Error(`self.node(): "${path.raw}::get" returned a record with no id`)
189
+ throw new Error(`self.node(): "${path.raw}" returned a record with no id`)
114
190
  }
115
191
  // Validated at the wire boundary: id is now known-present, props are
116
192
  // structurally checked. Bridge the parse output to the canonical `Node`.
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Shared binding helpers for auto-materialized members (functions + views).
3
+ *
4
+ * The worker ROUTE URL a member is served at — `<url>/<folderSlug>/<slug>` — is
5
+ * decoupled from the member's GRAPH layout (`/<origin>/{functions,views}/<slug>`,
6
+ * a fixed kernel convention). `folderSlug` names only the URL segment.
7
+ */
8
+
9
+ import type { FunctionBinding } from '@astrale-os/kernel-api/routed'
10
+
11
+ /** Join a worker-relative mount path onto the serving url (single slash). */
12
+ export function joinWorkerPath(url: string, mount: string): string {
13
+ const base = url.replace(/\/+$/, '')
14
+ return `${base}/${mount.replace(/^\/+/, '')}`
15
+ }
16
+
17
+ /**
18
+ * Resolve a member's effective binding against the serving `url`. An explicit
19
+ * `override.remoteUrl` wins; otherwise defaults to `<url>/<folderSlug>/<slug>`.
20
+ * Same trailing-slash discipline as {@link joinWorkerPath} — a `url` ending in
21
+ * `/` must not produce `//` (the kernel pins `iss` by exact string).
22
+ */
23
+ export function resolveBinding(
24
+ override: FunctionBinding | undefined,
25
+ url: string,
26
+ folderSlug: string,
27
+ slug: string,
28
+ ): FunctionBinding {
29
+ const base = url.replace(/\/+$/, '')
30
+ if (override) {
31
+ return {
32
+ ...override,
33
+ remoteUrl: override.remoteUrl ?? `${base}/${folderSlug}/${slug}`,
34
+ }
35
+ }
36
+ return { remoteUrl: `${base}/${folderSlug}/${slug}` }
37
+ }
@@ -18,9 +18,10 @@
18
18
  */
19
19
 
20
20
  import type { Graph, WireGraph } from '@astrale-os/kernel-core'
21
- import type { BoundMethod, FunctionSchema } from '@astrale-os/kernel-core/domain'
21
+ import type { BoundMethod, FunctionSchema, ViewSchema } from '@astrale-os/kernel-core/domain'
22
22
  import type { Schema } from '@astrale-os/kernel-dsl'
23
23
 
24
+ import { AbsolutePath } from '@astrale-os/kernel-core'
24
25
  import { hashInstallGraph, serialize, zodToJsonSchema } from '@astrale-os/kernel-core/domain'
25
26
 
26
27
  import type { AnyRemoteHandler } from '../method/single'
@@ -41,8 +42,23 @@ export function buildInstallGraph<S extends Schema>(
41
42
  domain: RemoteDomain<S>,
42
43
  url: string,
43
44
  ): WireGraph {
44
- const { compiled } = materializeRemoteDomain(domain, url)
45
- return buildSpecInternal(compiled, domain.methods, url).toWire() as WireGraph
45
+ const { compiled, auxiliary } = materializeRemoteDomain(domain, url)
46
+ // Two-axis placement: `parent` = the folder the Domain node physically lives
47
+ // under. `path` names the FULL node path (basename === origin);
48
+ // unset defaults to /domains/<origin> — the "/domains" placement policy lives
49
+ // HERE (the kernel default is Root → /<origin>). `mount` is deliberately NOT
50
+ // passed: installed_in + typed addresses stay root-mounted (kernel default).
51
+ const parent = domain.path
52
+ ? AbsolutePath.parse(domain.path).parent()
53
+ : AbsolutePath.from('domains')
54
+ return buildSpecInternal(
55
+ compiled,
56
+ domain.methods,
57
+ url,
58
+ auxiliary?.functionSchemas ?? [],
59
+ auxiliary?.viewSchemas ?? [],
60
+ parent,
61
+ ).toWire() as WireGraph
46
62
  }
47
63
 
48
64
  /**
@@ -63,12 +79,21 @@ function buildSpecInternal(
63
79
  compiled: RemoteDomain['compiled'],
64
80
  methods: BoundMethod<AnyRemoteHandler>[],
65
81
  url: string,
82
+ functionSchemas: FunctionSchema[],
83
+ viewSchemas: ViewSchema[],
84
+ parent: AbsolutePath,
66
85
  ): Graph {
67
86
  const serialized = serializeMethodsWithStubs(compiled, methods, url)
68
- const tree = serialize(compiled, serialized)
69
- // Views / RemoteFunctions are auto-materialized into the Core BEFORE
70
- // `compileDomain` runs (see `extend-core.ts`). By the time we reach
71
- // `serialize` here the Tree already contains their nodes/edges.
87
+ // Method impls + standalone-function-member impls travel in the same callable
88
+ // array (distinct refs: `class.X.method.y` vs `function.<slug>`); view-member
89
+ // impls travel in `options.views` (a view differs structurally — handshake,
90
+ // view_for, View class). The serializer emits method nodes from the IR,
91
+ // function/view members from `compiled.$.refs.{functions,views}`, pulling each
92
+ // impl by ref.
93
+ const tree = serialize(compiled, [...serialized, ...functionSchemas], {
94
+ views: viewSchemas,
95
+ parent,
96
+ })
72
97
  return tree.toGraph()
73
98
  }
74
99