@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.
Files changed (160) 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 +2 -0
  7. package/dist/auth/index.d.ts.map +1 -1
  8. package/dist/auth/index.js +1 -0
  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 -1
  35. package/dist/cli/run.d.ts.map +1 -1
  36. package/dist/cli/run.js +85 -101
  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/define-domain.d.ts +79 -2
  42. package/dist/config/define-domain.d.ts.map +1 -1
  43. package/dist/config/define-domain.js +58 -1
  44. package/dist/config/define-domain.js.map +1 -1
  45. package/dist/config/index.d.ts +1 -1
  46. package/dist/config/index.d.ts.map +1 -1
  47. package/dist/define/remote-function.d.ts +23 -3
  48. package/dist/define/remote-function.d.ts.map +1 -1
  49. package/dist/define/remote-function.js +1 -1
  50. package/dist/define/remote-function.js.map +1 -1
  51. package/dist/deploy/check.d.ts +6 -6
  52. package/dist/deploy/check.d.ts.map +1 -1
  53. package/dist/deploy/check.js +1 -1
  54. package/dist/deploy/check.js.map +1 -1
  55. package/dist/deploy/index.d.ts +0 -1
  56. package/dist/deploy/index.d.ts.map +1 -1
  57. package/dist/deploy/index.js +0 -1
  58. package/dist/deploy/index.js.map +1 -1
  59. package/dist/deploy/meta.d.ts +11 -0
  60. package/dist/deploy/meta.d.ts.map +1 -1
  61. package/dist/deploy/meta.js +23 -0
  62. package/dist/deploy/meta.js.map +1 -1
  63. package/dist/dispatch/dispatcher.d.ts +17 -4
  64. package/dist/dispatch/dispatcher.d.ts.map +1 -1
  65. package/dist/dispatch/dispatcher.js +15 -3
  66. package/dist/dispatch/dispatcher.js.map +1 -1
  67. package/dist/dispatch/execute.d.ts +6 -2
  68. package/dist/dispatch/execute.d.ts.map +1 -1
  69. package/dist/dispatch/execute.js +1 -0
  70. package/dist/dispatch/execute.js.map +1 -1
  71. package/dist/dispatch/identity.d.ts +11 -10
  72. package/dist/dispatch/identity.d.ts.map +1 -1
  73. package/dist/dispatch/identity.js +4 -3
  74. package/dist/dispatch/identity.js.map +1 -1
  75. package/dist/dispatch/index.d.ts +1 -1
  76. package/dist/dispatch/index.d.ts.map +1 -1
  77. package/dist/dispatch/index.js +1 -1
  78. package/dist/dispatch/index.js.map +1 -1
  79. package/dist/dispatch/self.d.ts +25 -37
  80. package/dist/dispatch/self.d.ts.map +1 -1
  81. package/dist/dispatch/self.js +60 -30
  82. package/dist/dispatch/self.js.map +1 -1
  83. package/dist/domain/build-spec.d.ts.map +1 -1
  84. package/dist/domain/build-spec.js +15 -3
  85. package/dist/domain/build-spec.js.map +1 -1
  86. package/dist/domain/define.d.ts +17 -32
  87. package/dist/domain/define.d.ts.map +1 -1
  88. package/dist/domain/define.js +12 -31
  89. package/dist/domain/define.js.map +1 -1
  90. package/dist/domain/extend-functions.d.ts +13 -9
  91. package/dist/domain/extend-functions.d.ts.map +1 -1
  92. package/dist/domain/extend-functions.js +28 -13
  93. package/dist/domain/extend-functions.js.map +1 -1
  94. package/dist/index.d.ts +7 -3
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +12 -4
  97. package/dist/index.js.map +1 -1
  98. package/dist/method/class.d.ts +2 -2
  99. package/dist/method/class.d.ts.map +1 -1
  100. package/dist/method/class.js.map +1 -1
  101. package/dist/method/context.d.ts +18 -3
  102. package/dist/method/context.d.ts.map +1 -1
  103. package/dist/method/index.d.ts +2 -2
  104. package/dist/method/index.d.ts.map +1 -1
  105. package/dist/method/index.js.map +1 -1
  106. package/dist/method/single.d.ts +25 -38
  107. package/dist/method/single.d.ts.map +1 -1
  108. package/dist/method/single.js +1 -11
  109. package/dist/method/single.js.map +1 -1
  110. package/dist/server/auxiliary-routes.d.ts.map +1 -1
  111. package/dist/server/auxiliary-routes.js +13 -2
  112. package/dist/server/auxiliary-routes.js.map +1 -1
  113. package/dist/server/create.d.ts +2 -15
  114. package/dist/server/create.d.ts.map +1 -1
  115. package/dist/server/create.js +15 -51
  116. package/dist/server/create.js.map +1 -1
  117. package/dist/server/domain-entry.d.ts +10 -0
  118. package/dist/server/domain-entry.d.ts.map +1 -1
  119. package/dist/server/domain-entry.js +2 -0
  120. package/dist/server/domain-entry.js.map +1 -1
  121. package/dist/server/index.d.ts +0 -1
  122. package/dist/server/index.d.ts.map +1 -1
  123. package/dist/server/index.js +0 -1
  124. package/dist/server/index.js.map +1 -1
  125. package/package.json +10 -8
  126. package/src/auth/check.ts +2 -6
  127. package/src/auth/index.ts +2 -0
  128. package/src/auth/issuer-mint.ts +75 -0
  129. package/src/auth/kernel-client.ts +31 -63
  130. package/src/cli/dotenv.ts +20 -0
  131. package/src/cli/index.ts +1 -1
  132. package/src/cli/log.ts +18 -0
  133. package/src/cli/publish.ts +341 -0
  134. package/src/cli/run.ts +92 -113
  135. package/src/cli/spec.ts +1 -0
  136. package/src/config/define-domain.ts +158 -5
  137. package/src/config/index.ts +1 -1
  138. package/src/define/remote-function.ts +47 -7
  139. package/src/deploy/check.ts +7 -7
  140. package/src/deploy/index.ts +0 -1
  141. package/src/deploy/meta.ts +25 -0
  142. package/src/dispatch/dispatcher.ts +32 -9
  143. package/src/dispatch/execute.ts +7 -2
  144. package/src/dispatch/identity.ts +11 -10
  145. package/src/dispatch/index.ts +1 -1
  146. package/src/dispatch/self.ts +117 -41
  147. package/src/domain/build-spec.ts +15 -1
  148. package/src/domain/define.ts +26 -38
  149. package/src/domain/extend-functions.ts +35 -15
  150. package/src/index.ts +29 -5
  151. package/src/method/class.ts +11 -7
  152. package/src/method/context.ts +19 -3
  153. package/src/method/index.ts +8 -2
  154. package/src/method/single.ts +35 -42
  155. package/src/server/auxiliary-routes.ts +13 -2
  156. package/src/server/create.ts +15 -51
  157. package/src/server/domain-entry.ts +12 -0
  158. package/src/server/index.ts +0 -1
  159. package/src/deploy/hash-spec.ts +0 -31
  160. 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 { RemoteFunctionDef, ViewDef } from '../define'
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
- type AnyFunctionDef = RemoteFunctionDef<any, any, any>
34
- // oxlint-enable no-explicit-any
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. "dist.astrale.ai"), not a URL.',
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
@@ -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 distribution `RemoteFunction`.)
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`. Nullability follows {@link KernelForAuth} of the
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
- // `TAuth = AuthPolicy` (the full union, not the `'required'` default) keeps this
117
- // permissive across any declared policy; `ctx.kernel` widens to
118
- // `BoundClientSessionView<FnMap> | null`. Used where the concrete policy is erased.
119
- // oxlint-disable-next-line no-explicit-any
120
- export type AnyRemoteFunctionDef = RemoteFunctionDef<any, any, any, AuthPolicy>
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
@@ -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://dist.astrale.ai`). */
9
+ /** Deployed worker URL (e.g. `https://example.astrale.ai`). */
10
10
  url: string
11
11
  /**
12
- * Expected `meta.schemaHash` — the hash of the locally-built `spec.json`
13
- * (via `hashSpecFile`). REQUIRED to verify schema drift: when the worker
14
- * advertises a `schemaHash` but this is absent, the check fails loudly rather
15
- * than silently "passing" an unverified deploy. Per-domain deploy scripts
16
- * compute it (`hashSpecFile('./spec.json')`) and pass 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. hashSpecFile('./spec.json'))`,
99
+ `cannot verify schema drift (pass expectedSchemaHash, e.g. buildInstallGraphHash(domain, url))`,
100
100
  )
101
101
  }
102
102
  if (meta.schemaHash !== opts.expectedSchemaHash) {
@@ -1,3 +1,2 @@
1
1
  export { MetaSchema, type Meta } from './meta'
2
- export { hashSpecFile } from './hash-spec'
3
2
  export { deployCheck, type DeployCheckOptions } from './check'
@@ -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 { 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
  }
@@ -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: Methods on classes/interfaces (sub = MethodPath), standalone-function
6
- * MEMBERS (sub = AbsolutePath at `/<origin>/functions/<slug>`), and view MEMBERS
7
- * (sub = AbsolutePath at `/<origin>/views/<slug>`).
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 → `AbsolutePath.raw` for each aux callable MEMBER node:
58
- * - standalone-function members (`kind: 'function'`) at `/<origin>/functions/<slug>`;
59
- * - view members (`kind: 'view'`) at `/<origin>/views/<slug>` (plus any
60
- * hand-authored/legacy core `View` nodes, `kind: 'core'` className `View`).
61
- * Neither has a class+method decomposition that would justify a MethodPath
62
- * their identity is their 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.
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` (the node's AbsolutePath). Mirrors `buildIdentityMap`.
86
+ * with its own `sub` (its FunctionPath/ViewPath). Mirrors `buildIdentityMap`.
86
87
  */
87
88
  export type AuxIdentityMap = AuxBuckets<RemoteIdentityConfig>
88
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 {