@astrale-os/sdk 0.2.0 → 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 +2 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +1 -0
- 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 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +85 -101
- 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/define-domain.d.ts +79 -2
- package/dist/config/define-domain.d.ts.map +1 -1
- package/dist/config/define-domain.js +58 -1
- package/dist/config/define-domain.js.map +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 +23 -3
- package/dist/define/remote-function.d.ts.map +1 -1
- package/dist/define/remote-function.js +1 -1
- package/dist/define/remote-function.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 +11 -10
- package/dist/dispatch/identity.d.ts.map +1 -1
- package/dist/dispatch/identity.js +4 -3
- 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/build-spec.d.ts.map +1 -1
- package/dist/domain/build-spec.js +15 -3
- package/dist/domain/build-spec.js.map +1 -1
- package/dist/domain/define.d.ts +17 -32
- package/dist/domain/define.d.ts.map +1 -1
- package/dist/domain/define.js +12 -31
- package/dist/domain/define.js.map +1 -1
- package/dist/domain/extend-functions.d.ts +13 -9
- package/dist/domain/extend-functions.d.ts.map +1 -1
- package/dist/domain/extend-functions.js +28 -13
- package/dist/domain/extend-functions.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -4
- 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 +13 -2
- 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/package.json +10 -8
- package/src/auth/check.ts +2 -6
- package/src/auth/index.ts +2 -0
- 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 +92 -113
- package/src/cli/spec.ts +1 -0
- package/src/config/define-domain.ts +158 -5
- package/src/config/index.ts +1 -1
- package/src/define/remote-function.ts +47 -7
- 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 +11 -10
- package/src/dispatch/index.ts +1 -1
- package/src/dispatch/self.ts +117 -41
- package/src/domain/build-spec.ts +15 -1
- package/src/domain/define.ts +26 -38
- package/src/domain/extend-functions.ts +35 -15
- package/src/index.ts +29 -5
- 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 +13 -2
- package/src/server/create.ts +15 -51
- package/src/server/domain-entry.ts +12 -0
- package/src/server/index.ts +0 -1
- package/src/deploy/hash-spec.ts +0 -31
- package/src/server/worker-meta.ts +0 -25
|
@@ -20,7 +20,7 @@ import type { Schema } from '@astrale-os/kernel-dsl'
|
|
|
20
20
|
|
|
21
21
|
import { DomainOrigin } from '@astrale-os/kernel-core/domain'
|
|
22
22
|
|
|
23
|
-
import type {
|
|
23
|
+
import type { AnyRemoteFunctionDef, ViewDef } from '../define'
|
|
24
24
|
import type { SchemaMethodsImpl } from '../method'
|
|
25
25
|
|
|
26
26
|
// A registry holds views / functions of every deps + param/result shape. Their
|
|
@@ -28,10 +28,69 @@ import type { SchemaMethodsImpl } from '../method'
|
|
|
28
28
|
// site; here they're held loosely and, crucially, kept OUT of `TDeps` inference
|
|
29
29
|
// — so `TDeps` is fixed by `methods` alone (the authoritative deps source) and a
|
|
30
30
|
// view authored with the default `unknown` deps can't fight `methods`'s `Env`.
|
|
31
|
-
// oxlint-disable no-explicit-any
|
|
31
|
+
// oxlint-disable-next-line no-explicit-any
|
|
32
32
|
type AnyViewDef = ViewDef<any>
|
|
33
|
-
|
|
34
|
-
//
|
|
33
|
+
// The function bag is the canonical, variance-correct one from `../define` (it
|
|
34
|
+
// must accept functions of any auth policy — see the note there). Aliased to
|
|
35
|
+
// keep the local usages below terse.
|
|
36
|
+
type AnyFunctionDef = AnyRemoteFunctionDef
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Optional presentation metadata for the domain, served verbatim on the
|
|
40
|
+
* worker's `/meta` endpoint (see `../deploy/meta`) so a host UI can brand a
|
|
41
|
+
* freshly-installed domain. Both the block and every field are optional.
|
|
42
|
+
*/
|
|
43
|
+
export interface DomainManifest {
|
|
44
|
+
/** Domain logo — an inline SVG string or a `data:` URL (no extra validation). */
|
|
45
|
+
logo?: string
|
|
46
|
+
/**
|
|
47
|
+
* Slug of one of THIS domain's own views to open first — the domain's entry
|
|
48
|
+
* surface. This is the SERVED form: a plain slug, stamped verbatim onto
|
|
49
|
+
* `/meta`. Authors don't write the slug directly — they pass the view OBJECT
|
|
50
|
+
* to `defineDomain` (see {@link DomainManifestConfig.entrypoint}), which
|
|
51
|
+
* resolves it to this slug. Pinned at app install to the `View` node at
|
|
52
|
+
* `/<origin>/views/<slug>` via the app's `entrypoint` edge.
|
|
53
|
+
*/
|
|
54
|
+
entrypoint?: string
|
|
55
|
+
/**
|
|
56
|
+
* The ROLES this domain declares — named capability identities a workspace
|
|
57
|
+
* registers when an app for this domain is installed, then assigns to users
|
|
58
|
+
* (`default: true` roles are auto-assigned to the installing user). The
|
|
59
|
+
* domain itself grants each role its resource permissions (typically in its
|
|
60
|
+
* `postInstall`); declaring a role here only names it. Slugs are validated
|
|
61
|
+
* (lowercase slug charset, unique) at `defineDomain`.
|
|
62
|
+
*/
|
|
63
|
+
roles?: readonly DomainRoleDecl[]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** One declared role of the domain (see {@link DomainManifest.roles}). */
|
|
67
|
+
export interface DomainRoleDecl {
|
|
68
|
+
/** Stable role slug, unique within the domain (e.g. `"editor"`). */
|
|
69
|
+
slug: string
|
|
70
|
+
/** Display name; defaults to the slug. */
|
|
71
|
+
name?: string
|
|
72
|
+
/** What holding this role means — shown in permission UIs. */
|
|
73
|
+
description?: string
|
|
74
|
+
/** Auto-assign this role to the user installing an app for this domain. */
|
|
75
|
+
default?: boolean
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The author-facing manifest accepted by {@link defineDomain} — identical to the
|
|
80
|
+
* served {@link DomainManifest} except `entrypoint` is the view OBJECT (a value
|
|
81
|
+
* of the `views` map) rather than its slug. `defineDomain` resolves it to the
|
|
82
|
+
* slug by identity, so a renamed or dropped view is a compile error at the
|
|
83
|
+
* reference site, never a stale string (the same guarantee `postInstall` gives
|
|
84
|
+
* for functions).
|
|
85
|
+
*/
|
|
86
|
+
export interface DomainManifestConfig extends Omit<DomainManifest, 'entrypoint'> {
|
|
87
|
+
/**
|
|
88
|
+
* The view to open first — the domain's entry surface. Pass the view OBJECT
|
|
89
|
+
* from this domain's own `views` map (e.g. `entrypoint: views.welcome`).
|
|
90
|
+
* Resolved to its slug here and served as {@link DomainManifest.entrypoint}.
|
|
91
|
+
*/
|
|
92
|
+
entrypoint?: AnyViewDef
|
|
93
|
+
}
|
|
35
94
|
|
|
36
95
|
export interface DefineDomainConfig<S extends Schema, TDeps, TEnv = unknown> {
|
|
37
96
|
/** The domain schema (from `schema/`). Its `.domain` seeds the default origin. */
|
|
@@ -67,6 +126,15 @@ export interface DefineDomainConfig<S extends Schema, TDeps, TEnv = unknown> {
|
|
|
67
126
|
* by slug. Omit when the domain has none.
|
|
68
127
|
*/
|
|
69
128
|
functions?: Record<string, AnyFunctionDef>
|
|
129
|
+
/**
|
|
130
|
+
* Optional presentation metadata stamped onto `/meta`. When `manifest.entrypoint`
|
|
131
|
+
* is set it MUST be a view from this domain's own `views` map — passed as the
|
|
132
|
+
* OBJECT (`entrypoint: views.welcome`), resolved to its slug here. A view that
|
|
133
|
+
* isn't in `views` is a compile error at the reference site, and the
|
|
134
|
+
* resolution throws loudly if it's somehow absent — so a typo can never dangle
|
|
135
|
+
* to a NOT_FOUND view path at install.
|
|
136
|
+
*/
|
|
137
|
+
manifest?: DomainManifestConfig
|
|
70
138
|
/**
|
|
71
139
|
* The domain's **addressing name** (the graph slug it mounts under, e.g.
|
|
72
140
|
* `'crm.acme.dev'`). Defaults to `schema.domain`. Must be a name, never a
|
|
@@ -78,6 +146,16 @@ export interface DefineDomainConfig<S extends Schema, TDeps, TEnv = unknown> {
|
|
|
78
146
|
origin?: string
|
|
79
147
|
/** Cross-domain deps, by origin. Verified present on the instance at install. */
|
|
80
148
|
requires?: readonly string[]
|
|
149
|
+
/**
|
|
150
|
+
* Where the Domain node physically lives in the graph TREE — an absolute tree
|
|
151
|
+
* path whose LAST segment is the origin (e.g. `'/domains/crm.acme.dev'`).
|
|
152
|
+
* Optional; defaults to `/domains/<origin>`. This moves ONLY the physical
|
|
153
|
+
* `has_parent` position: the domain's `installed_in` edge and EVERY typed
|
|
154
|
+
* address (`/:<origin>`, `/:<origin>:Class`, …) stay ROOT-mounted, so
|
|
155
|
+
* addressing is unchanged. A platform domain that must stay top-level sets its
|
|
156
|
+
* own origin path (e.g. `'/workspace.astrale.ai'`).
|
|
157
|
+
*/
|
|
158
|
+
path?: string
|
|
81
159
|
/**
|
|
82
160
|
* The function the kernel runs once after install, as __SYSTEM__ — where the
|
|
83
161
|
* domain seeds itself / posts its own grants. Reference it from the `functions`
|
|
@@ -103,9 +181,13 @@ export interface DomainDefinition {
|
|
|
103
181
|
deps?: (env: any, url: string) => any
|
|
104
182
|
views?: Record<string, ViewDef>
|
|
105
183
|
functions?: Record<string, AnyFunctionDef>
|
|
184
|
+
/** Presentation metadata for `/meta`, validated at definition time. */
|
|
185
|
+
manifest?: DomainManifest
|
|
106
186
|
origin: string
|
|
107
187
|
requires: readonly string[]
|
|
108
188
|
postInstall?: string
|
|
189
|
+
/** Physical tree path for the Domain node; default `/domains/<origin>`. */
|
|
190
|
+
path?: string
|
|
109
191
|
}
|
|
110
192
|
|
|
111
193
|
export function defineDomain<S extends Schema, TDeps, TEnv = unknown>(
|
|
@@ -146,25 +228,96 @@ export function defineDomain<S extends Schema, TDeps, TEnv = unknown>(
|
|
|
146
228
|
} catch {
|
|
147
229
|
throw new Error(
|
|
148
230
|
`defineDomain: invalid \`requires\` entry "${dep}". Use the dependency's origin slug ` +
|
|
149
|
-
'(lowercase FQDN-like, e.g. "
|
|
231
|
+
'(lowercase FQDN-like, e.g. "example.astrale.ai"), not a URL.',
|
|
150
232
|
)
|
|
151
233
|
}
|
|
152
234
|
})
|
|
153
235
|
|
|
236
|
+
// `path`, when set, is the Domain's physical tree path. Validate the
|
|
237
|
+
// two invariants the serializer/kernel rely on, string-only (no AbsolutePath
|
|
238
|
+
// import — this module must stay worker-safe): it is absolute with at least one
|
|
239
|
+
// segment, and its LAST segment equals the origin (the kernel derives origin
|
|
240
|
+
// from the mount basename in a few places, e.g. has_parent slug + get-access-
|
|
241
|
+
// token origin recovery). Full structural parse happens at build time.
|
|
242
|
+
if (config.path !== undefined) {
|
|
243
|
+
const loc = config.path
|
|
244
|
+
if (!loc.startsWith('/') || loc === '/') {
|
|
245
|
+
throw new Error(
|
|
246
|
+
`defineDomain: \`path\` must be an absolute graph path with at least ` +
|
|
247
|
+
`one segment (e.g. "/domains/${origin}"), not "${loc}".`,
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
if (loc.split('/').pop() !== origin) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
`defineDomain: \`path\` last segment must equal the origin "${origin}" ` +
|
|
253
|
+
`(e.g. "/domains/${origin}" or "/${origin}"), got "${loc}".`,
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// A `manifest.entrypoint` view is resolved to its slug below (in the return),
|
|
259
|
+
// where a view absent from `views` throws — see `resolveManifest`.
|
|
260
|
+
|
|
261
|
+
// Declared roles must carry valid, unique slugs — a role slug becomes a graph
|
|
262
|
+
// path segment and an identity subject, so the kernel's reserved separators
|
|
263
|
+
// (`/`, `:`, `@`) can never appear in one. Fail at authoring time.
|
|
264
|
+
if (config.manifest?.roles !== undefined) {
|
|
265
|
+
const seen = new Set<string>()
|
|
266
|
+
for (const role of config.manifest.roles) {
|
|
267
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/.test(role.slug)) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
`defineDomain: invalid \`manifest.roles\` slug "${role.slug}". ` +
|
|
270
|
+
'Use a lowercase slug (letters, digits, "_", "-"), e.g. "editor".',
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
if (seen.has(role.slug)) {
|
|
274
|
+
throw new Error(`defineDomain: duplicate \`manifest.roles\` slug "${role.slug}".`)
|
|
275
|
+
}
|
|
276
|
+
seen.add(role.slug)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
154
280
|
return {
|
|
155
281
|
schema: config.schema,
|
|
156
282
|
methods: config.methods as DomainDefinition['methods'],
|
|
157
283
|
...(config.deps ? { deps: config.deps as DomainDefinition['deps'] } : {}),
|
|
158
284
|
...(config.views ? { views: config.views as Record<string, ViewDef> } : {}),
|
|
159
285
|
...(config.functions ? { functions: config.functions } : {}),
|
|
286
|
+
...(config.manifest ? { manifest: resolveManifest(config.manifest, config.views) } : {}),
|
|
160
287
|
origin,
|
|
161
288
|
requires,
|
|
289
|
+
...(config.path ? { path: config.path } : {}),
|
|
162
290
|
...(config.postInstall !== undefined
|
|
163
291
|
? { postInstall: normalizePostInstall(config.postInstall, origin, config.functions) }
|
|
164
292
|
: {}),
|
|
165
293
|
}
|
|
166
294
|
}
|
|
167
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Lower the author-facing manifest (view OBJECT for `entrypoint`) to the served
|
|
298
|
+
* {@link DomainManifest} (slug for `entrypoint`). The entrypoint view is matched
|
|
299
|
+
* to its `views`-map key by IDENTITY — the same mechanism `normalizePostInstall`
|
|
300
|
+
* uses for functions — so a view absent from `views` (one from another domain,
|
|
301
|
+
* or dropped) throws here instead of dangling to a NOT_FOUND view path at install.
|
|
302
|
+
*/
|
|
303
|
+
function resolveManifest(
|
|
304
|
+
manifest: DomainManifestConfig,
|
|
305
|
+
views: Record<string, AnyViewDef> | undefined,
|
|
306
|
+
): DomainManifest {
|
|
307
|
+
const { entrypoint, ...rest } = manifest
|
|
308
|
+
if (entrypoint === undefined) return rest
|
|
309
|
+
const slug = views ? Object.entries(views).find(([, def]) => def === entrypoint)?.[0] : undefined
|
|
310
|
+
if (slug === undefined) {
|
|
311
|
+
const viewKeys = Object.keys(views ?? {})
|
|
312
|
+
throw new Error(
|
|
313
|
+
"defineDomain: `manifest.entrypoint` must be one of this domain's own views " +
|
|
314
|
+
'(pass the view object, e.g. `entrypoint: views.welcome`). ' +
|
|
315
|
+
`Available views: ${viewKeys.length > 0 ? viewKeys.map((k) => `"${k}"`).join(', ') : '(none)'}.`,
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
return { ...rest, entrypoint: slug }
|
|
319
|
+
}
|
|
320
|
+
|
|
168
321
|
/**
|
|
169
322
|
* Resolve a `postInstall` function reference to the colon-path the bundle carries.
|
|
170
323
|
* The slug is the `functions` map key the reference is registered under (found by
|
package/src/config/index.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
export { defineDomain } from './define-domain'
|
|
17
|
-
export type { DefineDomainConfig, DomainDefinition } from './define-domain'
|
|
17
|
+
export type { DefineDomainConfig, DomainDefinition, DomainManifest } from './define-domain'
|
|
18
18
|
|
|
19
19
|
export { deploy } from './deploy'
|
|
20
20
|
export type { DeployConfig } from './deploy'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Authoring a standalone remote function — a callable not bound to a class.
|
|
3
3
|
* (The verb keeps the `defineRemoteFunction` name; the node it materializes is
|
|
4
|
-
* the canonical kernel `Function` class, the former
|
|
4
|
+
* the canonical kernel `Function` class, replacing the former remote-function wrapper.)
|
|
5
5
|
*
|
|
6
6
|
* Each entry in `defineRemoteDomain({ remoteFunctions: { ... } })` becomes:
|
|
7
7
|
* - a graph node at `/${origin}/functions/<slug>` — a first-class domain MEMBER,
|
|
@@ -23,6 +23,7 @@ import type { BoundClientSessionView } from '@astrale-os/kernel-client/session'
|
|
|
23
23
|
import type { Context } from 'hono'
|
|
24
24
|
import type { z } from 'zod'
|
|
25
25
|
|
|
26
|
+
import type { DomainIssuer } from '../auth/issuer-mint'
|
|
26
27
|
import type { CallRemoteFn } from '../dispatch/call-remote'
|
|
27
28
|
import type { AuthForPolicy, KernelForAuth } from '../method/context'
|
|
28
29
|
|
|
@@ -47,12 +48,20 @@ export type RemoteFunctionContext<
|
|
|
47
48
|
/**
|
|
48
49
|
* `BoundClientSessionView` to the parent kernel, bound to the composed
|
|
49
50
|
* credential `union(delegation, self)` — same shape as `RemoteContext.kernel`
|
|
50
|
-
* for `remoteMethod
|
|
51
|
+
* for `remoteMethod`, including the typed graph read/write sugar flattened onto
|
|
52
|
+
* it (`kernel.get` / `kernel.children` / `kernel.query`,
|
|
53
|
+
* `kernel.createNode` / `kernel.mutate(patch)`, …) over the `function.get` /
|
|
54
|
+
* `function.mutate` syscalls. Nullability follows {@link KernelForAuth} of the
|
|
51
55
|
* function's `auth`: non-null for the default `'required'`, `… | null` for
|
|
52
56
|
* `'optional'`, `null` for `'public'` (use {@link RemoteFunctionContext.selfKernel}
|
|
53
57
|
* there, after verifying the upstream).
|
|
54
58
|
*/
|
|
55
59
|
kernel: TKernel
|
|
60
|
+
/**
|
|
61
|
+
* Mint seam for identities THIS DOMAIN issues — same contract as
|
|
62
|
+
* {@link RemoteContext.issuer}.
|
|
63
|
+
*/
|
|
64
|
+
issuer: DomainIssuer
|
|
56
65
|
/**
|
|
57
66
|
* Call another worker's remote method, re-minting the credential for the
|
|
58
67
|
* target's audience. Same contract as {@link RemoteContext.callRemote}: throws
|
|
@@ -113,11 +122,42 @@ export type RemoteFunctionDef<
|
|
|
113
122
|
description?: string
|
|
114
123
|
}
|
|
115
124
|
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
|
|
125
|
+
// Loosely-typed "bag" for the per-domain function maps. The PRECISE typing is
|
|
126
|
+
// enforced at each `defineRemoteFunction` call site; this only has to HOLD defs
|
|
127
|
+
// of ANY auth policy. We deliberately do NOT pin a single concrete `TAuth`:
|
|
128
|
+
// `RemoteFunctionContext` carries `kernel: KernelForAuth<TAuth>` (an
|
|
129
|
+
// auth-dependent type that resolves to a concrete union), so any single concrete
|
|
130
|
+
// policy — `'required'` OR the full `AuthPolicy` union — rejects the others when
|
|
131
|
+
// a handler is assigned into the bag under parameter contravariance (a public
|
|
132
|
+
// handler wants `kernel: null`, a required one `kernel: Kernel`; neither fits a
|
|
133
|
+
// fixed bag). Relaxing the `authorize`/`execute` PARAMS to `any` is what makes
|
|
134
|
+
// the bag accept every policy; soundness is unchanged — the bag was already
|
|
135
|
+
// `any, any, any`, and the real contract is checked at the call site.
|
|
136
|
+
// oxlint-disable no-explicit-any
|
|
137
|
+
export type AnyRemoteFunctionDef = Omit<
|
|
138
|
+
RemoteFunctionDef<any, any, any, AuthPolicy>,
|
|
139
|
+
'authorize' | 'execute'
|
|
140
|
+
> & {
|
|
141
|
+
authorize?: (ctx: any) => void | Promise<void>
|
|
142
|
+
execute: (ctx: any) => any
|
|
143
|
+
}
|
|
144
|
+
// oxlint-enable no-explicit-any
|
|
145
|
+
|
|
146
|
+
// Compile-time regression guards (pure `type` aliases — zero runtime emit, but
|
|
147
|
+
// checked by `pnpm typecheck` since this file is under `src/`). Each asserts a
|
|
148
|
+
// function of the given auth policy still FITS the bag. If `AnyRemoteFunctionDef`
|
|
149
|
+
// ever narrows back and rejects a policy, the matching `_Assert<false>` errors.
|
|
150
|
+
type _AssertTrue<T extends true> = T
|
|
151
|
+
type _FnFitsBag<D> = D extends AnyRemoteFunctionDef ? true : false
|
|
152
|
+
// Exported only so `noUnusedLocals` treats the assertions as used — it is NOT
|
|
153
|
+
// re-exported by `../define`, so it never reaches the package's public surface.
|
|
154
|
+
// A regression that makes the bag reject a policy turns the matching
|
|
155
|
+
// `_AssertTrue<false>` into a compile error here.
|
|
156
|
+
export type _AuthPolicyBagGuards = [
|
|
157
|
+
_AssertTrue<_FnFitsBag<RemoteFunctionDef<unknown, unknown, unknown, 'public'>>>,
|
|
158
|
+
_AssertTrue<_FnFitsBag<RemoteFunctionDef<unknown, unknown, unknown, 'optional'>>>,
|
|
159
|
+
_AssertTrue<_FnFitsBag<RemoteFunctionDef<unknown, unknown, unknown, 'required'>>>,
|
|
160
|
+
]
|
|
121
161
|
|
|
122
162
|
/**
|
|
123
163
|
* Identity helper for authoring a RemoteFunction. Returns its argument
|
package/src/deploy/check.ts
CHANGED
|
@@ -6,14 +6,14 @@ import type { Meta } from './meta'
|
|
|
6
6
|
import { MetaSchema } from './meta'
|
|
7
7
|
|
|
8
8
|
export type DeployCheckOptions = {
|
|
9
|
-
/** Deployed worker URL (e.g. `https://
|
|
9
|
+
/** Deployed worker URL (e.g. `https://example.astrale.ai`). */
|
|
10
10
|
url: string
|
|
11
11
|
/**
|
|
12
|
-
* Expected `meta.schemaHash` — the hash of the
|
|
13
|
-
* (
|
|
14
|
-
* advertises a `schemaHash` but this is absent, the check fails
|
|
15
|
-
* than silently "passing" an unverified deploy. Per-domain
|
|
16
|
-
* compute it (`
|
|
12
|
+
* Expected `meta.schemaHash` — the hash of the domain's install graph (via
|
|
13
|
+
* `buildInstallGraphHash(domain, url)`). REQUIRED to verify schema drift: when
|
|
14
|
+
* the worker advertises a `schemaHash` but this is absent, the check fails
|
|
15
|
+
* loudly rather than silently "passing" an unverified deploy. Per-domain
|
|
16
|
+
* deploy scripts compute it (`buildInstallGraphHash(...)`) and pass it.
|
|
17
17
|
*/
|
|
18
18
|
expectedSchemaHash?: string
|
|
19
19
|
/**
|
|
@@ -96,7 +96,7 @@ async function checkSchemaHash(
|
|
|
96
96
|
if (!opts.expectedSchemaHash) {
|
|
97
97
|
throw new Error(
|
|
98
98
|
`worker advertises schemaHash=${meta.schemaHash} but no expectedSchemaHash was provided — ` +
|
|
99
|
-
`cannot verify schema drift (pass expectedSchemaHash, e.g.
|
|
99
|
+
`cannot verify schema drift (pass expectedSchemaHash, e.g. buildInstallGraphHash(domain, url))`,
|
|
100
100
|
)
|
|
101
101
|
}
|
|
102
102
|
if (meta.schemaHash !== opts.expectedSchemaHash) {
|
package/src/deploy/index.ts
CHANGED
package/src/deploy/meta.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
import { z } from 'zod'
|
|
12
12
|
|
|
13
13
|
export const MetaSchema = z.object({
|
|
14
|
+
// The version of the meta payload. Incremented on any change to the payload
|
|
15
|
+
version: z.number().int().min(1).default(1),
|
|
14
16
|
/** Base URL where JWKS is published. Always stamped by `createRemoteServer`
|
|
15
17
|
* and required by `deployCheck` to verify JWKS reachability. */
|
|
16
18
|
iss: z.string().min(1),
|
|
@@ -20,6 +22,29 @@ export const MetaSchema = z.object({
|
|
|
20
22
|
schemaHash: z.string().optional(),
|
|
21
23
|
/** Local directory name under `kernel/domains/<...>/` — used to auto-resolve the local spec. */
|
|
22
24
|
domainName: z.string().optional(),
|
|
25
|
+
/**
|
|
26
|
+
* Optional presentation metadata declared on the domain (`defineDomain`):
|
|
27
|
+
* `logo` (inline SVG / `data:` URL), `entrypoint` (a view slug — the domain's
|
|
28
|
+
* entry surface), and the domain's declared `roles` (registered by a workspace
|
|
29
|
+
* at app install). Served verbatim so a host UI can brand a freshly-installed
|
|
30
|
+
* domain.
|
|
31
|
+
*/
|
|
32
|
+
manifest: z
|
|
33
|
+
.object({
|
|
34
|
+
logo: z.string().optional(),
|
|
35
|
+
entrypoint: z.string().optional(),
|
|
36
|
+
roles: z
|
|
37
|
+
.array(
|
|
38
|
+
z.object({
|
|
39
|
+
slug: z.string().min(1),
|
|
40
|
+
name: z.string().optional(),
|
|
41
|
+
description: z.string().optional(),
|
|
42
|
+
default: z.boolean().optional(),
|
|
43
|
+
}),
|
|
44
|
+
)
|
|
45
|
+
.optional(),
|
|
46
|
+
})
|
|
47
|
+
.optional(),
|
|
23
48
|
})
|
|
24
49
|
|
|
25
50
|
export type Meta = z.infer<typeof MetaSchema>
|
|
@@ -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
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Per-callable identity — dispatcher runtime + install-time wiring.
|
|
3
3
|
*
|
|
4
4
|
* Three flavors of callable get an identity per the install-time identity
|
|
5
|
-
* binding
|
|
6
|
-
*
|
|
7
|
-
* (sub =
|
|
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,12 +55,12 @@ export type AuxBuckets<T> = {
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
|
-
* slug → `
|
|
58
|
-
* - standalone-function members (`kind: 'function'`)
|
|
59
|
-
* - view members (`kind: 'view'`)
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
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.
|
|
63
64
|
*/
|
|
64
65
|
export type AuxIdentityPaths = AuxBuckets<string>
|
|
65
66
|
|
|
@@ -82,7 +83,7 @@ export function collectAuxIdentityPaths(compiled: CompiledDomain): AuxIdentityPa
|
|
|
82
83
|
* slug → `RemoteIdentityConfig` for each auto-materialized View /
|
|
83
84
|
* RemoteFunction. Built once at server startup; consumed by
|
|
84
85
|
* `mountAuxiliaryRoutes` so each handler signs outbound `kernel.call(...)`
|
|
85
|
-
* with its own `sub` (
|
|
86
|
+
* with its own `sub` (its FunctionPath/ViewPath). Mirrors `buildIdentityMap`.
|
|
86
87
|
*/
|
|
87
88
|
export type AuxIdentityMap = AuxBuckets<RemoteIdentityConfig>
|
|
88
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 {
|