@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.
- package/LICENSE +202 -0
- package/dist/auth/check.d.ts +1 -2
- package/dist/auth/check.d.ts.map +1 -1
- package/dist/auth/check.js +2 -6
- package/dist/auth/check.js.map +1 -1
- package/dist/auth/index.d.ts +3 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +2 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/issuer-mint.d.ts +53 -0
- package/dist/auth/issuer-mint.d.ts.map +1 -0
- package/dist/auth/issuer-mint.js +35 -0
- package/dist/auth/issuer-mint.js.map +1 -0
- package/dist/auth/kernel-client.d.ts +8 -2
- package/dist/auth/kernel-client.d.ts.map +1 -1
- package/dist/auth/kernel-client.js +22 -46
- package/dist/auth/kernel-client.js.map +1 -1
- package/dist/cli/dotenv.d.ts +8 -0
- package/dist/cli/dotenv.d.ts.map +1 -1
- package/dist/cli/dotenv.js +18 -0
- package/dist/cli/dotenv.js.map +1 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/log.d.ts +11 -0
- package/dist/cli/log.d.ts.map +1 -0
- package/dist/cli/log.js +17 -0
- package/dist/cli/log.js.map +1 -0
- package/dist/cli/publish.d.ts +35 -0
- package/dist/cli/publish.d.ts.map +1 -0
- package/dist/cli/publish.js +271 -0
- package/dist/cli/publish.js.map +1 -0
- package/dist/cli/run.d.ts +17 -2
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +102 -111
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/spec.d.ts.map +1 -1
- package/dist/cli/spec.js +1 -0
- package/dist/cli/spec.js.map +1 -1
- package/dist/config/adapter.d.ts +12 -11
- package/dist/config/adapter.d.ts.map +1 -1
- package/dist/config/adapter.js.map +1 -1
- package/dist/config/define-domain.d.ts +88 -27
- package/dist/config/define-domain.d.ts.map +1 -1
- package/dist/config/define-domain.js +79 -33
- package/dist/config/define-domain.js.map +1 -1
- package/dist/config/deploy.d.ts +1 -1
- package/dist/config/deploy.js +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/define/remote-function.d.ts +32 -17
- package/dist/define/remote-function.d.ts.map +1 -1
- package/dist/define/remote-function.js +10 -9
- package/dist/define/remote-function.js.map +1 -1
- package/dist/define/view.d.ts +8 -6
- package/dist/define/view.d.ts.map +1 -1
- package/dist/define/view.js +8 -6
- package/dist/define/view.js.map +1 -1
- package/dist/deploy/check.d.ts +6 -6
- package/dist/deploy/check.d.ts.map +1 -1
- package/dist/deploy/check.js +1 -1
- package/dist/deploy/check.js.map +1 -1
- package/dist/deploy/index.d.ts +0 -1
- package/dist/deploy/index.d.ts.map +1 -1
- package/dist/deploy/index.js +0 -1
- package/dist/deploy/index.js.map +1 -1
- package/dist/deploy/meta.d.ts +11 -0
- package/dist/deploy/meta.d.ts.map +1 -1
- package/dist/deploy/meta.js +23 -0
- package/dist/deploy/meta.js.map +1 -1
- package/dist/dispatch/dispatcher.d.ts +17 -4
- package/dist/dispatch/dispatcher.d.ts.map +1 -1
- package/dist/dispatch/dispatcher.js +15 -3
- package/dist/dispatch/dispatcher.js.map +1 -1
- package/dist/dispatch/execute.d.ts +6 -2
- package/dist/dispatch/execute.d.ts.map +1 -1
- package/dist/dispatch/execute.js +1 -0
- package/dist/dispatch/execute.js.map +1 -1
- package/dist/dispatch/identity.d.ts +12 -10
- package/dist/dispatch/identity.d.ts.map +1 -1
- package/dist/dispatch/identity.js +10 -15
- package/dist/dispatch/identity.js.map +1 -1
- package/dist/dispatch/index.d.ts +1 -1
- package/dist/dispatch/index.d.ts.map +1 -1
- package/dist/dispatch/index.js +1 -1
- package/dist/dispatch/index.js.map +1 -1
- package/dist/dispatch/self.d.ts +25 -37
- package/dist/dispatch/self.d.ts.map +1 -1
- package/dist/dispatch/self.js +60 -30
- package/dist/dispatch/self.js.map +1 -1
- package/dist/domain/binding.d.ts +18 -0
- package/dist/domain/binding.d.ts.map +1 -0
- package/dist/domain/binding.js +29 -0
- package/dist/domain/binding.js.map +1 -0
- package/dist/domain/build-spec.d.ts.map +1 -1
- package/dist/domain/build-spec.js +22 -7
- package/dist/domain/build-spec.js.map +1 -1
- package/dist/domain/define.d.ts +28 -43
- package/dist/domain/define.d.ts.map +1 -1
- package/dist/domain/define.js +23 -60
- package/dist/domain/define.js.map +1 -1
- package/dist/domain/extend-functions.d.ts +48 -0
- package/dist/domain/extend-functions.d.ts.map +1 -0
- package/dist/domain/extend-functions.js +84 -0
- package/dist/domain/extend-functions.js.map +1 -0
- package/dist/domain/extend-views.d.ts +45 -0
- package/dist/domain/extend-views.d.ts.map +1 -0
- package/dist/domain/extend-views.js +116 -0
- package/dist/domain/extend-views.js.map +1 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -5
- package/dist/index.js.map +1 -1
- package/dist/method/class.d.ts +2 -2
- package/dist/method/class.d.ts.map +1 -1
- package/dist/method/class.js.map +1 -1
- package/dist/method/context.d.ts +18 -3
- package/dist/method/context.d.ts.map +1 -1
- package/dist/method/index.d.ts +2 -2
- package/dist/method/index.d.ts.map +1 -1
- package/dist/method/index.js.map +1 -1
- package/dist/method/single.d.ts +25 -38
- package/dist/method/single.d.ts.map +1 -1
- package/dist/method/single.js +1 -11
- package/dist/method/single.js.map +1 -1
- package/dist/server/auxiliary-routes.d.ts.map +1 -1
- package/dist/server/auxiliary-routes.js +14 -3
- package/dist/server/auxiliary-routes.js.map +1 -1
- package/dist/server/create.d.ts +2 -15
- package/dist/server/create.d.ts.map +1 -1
- package/dist/server/create.js +15 -51
- package/dist/server/create.js.map +1 -1
- package/dist/server/domain-entry.d.ts +10 -0
- package/dist/server/domain-entry.d.ts.map +1 -1
- package/dist/server/domain-entry.js +2 -0
- package/dist/server/domain-entry.js.map +1 -1
- package/dist/server/index.d.ts +0 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +0 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/worker-entry.d.ts +3 -3
- package/dist/server/worker-entry.js +3 -3
- package/package.json +10 -11
- package/src/auth/check.ts +2 -6
- package/src/auth/index.ts +3 -1
- package/src/auth/issuer-mint.ts +75 -0
- package/src/auth/kernel-client.ts +31 -63
- package/src/cli/dotenv.ts +20 -0
- package/src/cli/index.ts +1 -1
- package/src/cli/log.ts +18 -0
- package/src/cli/publish.ts +341 -0
- package/src/cli/run.ts +109 -125
- package/src/cli/spec.ts +1 -0
- package/src/config/adapter.ts +9 -11
- package/src/config/define-domain.ts +184 -53
- package/src/config/deploy.ts +1 -1
- package/src/config/index.ts +1 -1
- package/src/define/remote-function.ts +56 -21
- package/src/define/view.ts +8 -6
- package/src/deploy/check.ts +7 -7
- package/src/deploy/index.ts +0 -1
- package/src/deploy/meta.ts +25 -0
- package/src/dispatch/dispatcher.ts +32 -9
- package/src/dispatch/execute.ts +7 -2
- package/src/dispatch/identity.ts +17 -22
- package/src/dispatch/index.ts +1 -1
- package/src/dispatch/self.ts +117 -41
- package/src/domain/binding.ts +37 -0
- package/src/domain/build-spec.ts +32 -7
- package/src/domain/define.ts +71 -78
- package/src/domain/extend-functions.ts +106 -0
- package/src/domain/extend-views.ts +151 -0
- package/src/index.ts +36 -7
- package/src/method/class.ts +11 -7
- package/src/method/context.ts +19 -3
- package/src/method/index.ts +8 -2
- package/src/method/single.ts +35 -42
- package/src/server/auxiliary-routes.ts +14 -3
- package/src/server/create.ts +15 -51
- package/src/server/domain-entry.ts +12 -0
- package/src/server/index.ts +0 -1
- package/src/server/worker-entry.ts +3 -3
- package/src/deploy/hash-spec.ts +0 -31
- package/src/domain/extend-core.ts +0 -301
- 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 {
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
239
|
+
function pathToString(path: PathInput): string {
|
|
217
240
|
return typeof path === 'string' ? path : path.raw
|
|
218
241
|
}
|
|
219
242
|
|
package/src/dispatch/execute.ts
CHANGED
|
@@ -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
|
|
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
|
}
|
package/src/dispatch/identity.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-callable identity — dispatcher runtime + install-time wiring.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* binding
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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 → `
|
|
58
|
-
* (
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
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 (
|
|
70
|
-
if (c.
|
|
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
|
-
|
|
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` (
|
|
86
|
+
* with its own `sub` (its FunctionPath/ViewPath). Mirrors `buildIdentityMap`.
|
|
92
87
|
*/
|
|
93
88
|
export type AuxIdentityMap = AuxBuckets<RemoteIdentityConfig>
|
|
94
89
|
|
package/src/dispatch/index.ts
CHANGED
|
@@ -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 {
|
package/src/dispatch/self.ts
CHANGED
|
@@ -1,30 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Self resolution
|
|
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 {
|
|
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
|
-
*
|
|
47
|
-
*
|
|
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
|
-
|
|
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}
|
|
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
|
+
}
|
package/src/domain/build-spec.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
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
|
|