@astrale-os/sdk 0.1.10 → 0.2.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 (77) hide show
  1. package/dist/auth/index.d.ts +1 -1
  2. package/dist/auth/index.d.ts.map +1 -1
  3. package/dist/auth/index.js +1 -1
  4. package/dist/auth/index.js.map +1 -1
  5. package/dist/cli/run.d.ts +0 -1
  6. package/dist/cli/run.d.ts.map +1 -1
  7. package/dist/cli/run.js +17 -10
  8. package/dist/cli/run.js.map +1 -1
  9. package/dist/config/adapter.d.ts +12 -11
  10. package/dist/config/adapter.d.ts.map +1 -1
  11. package/dist/config/adapter.js.map +1 -1
  12. package/dist/config/define-domain.d.ts +13 -29
  13. package/dist/config/define-domain.d.ts.map +1 -1
  14. package/dist/config/define-domain.js +22 -33
  15. package/dist/config/define-domain.js.map +1 -1
  16. package/dist/config/deploy.d.ts +1 -1
  17. package/dist/config/deploy.js +1 -1
  18. package/dist/config/index.d.ts +1 -1
  19. package/dist/config/index.d.ts.map +1 -1
  20. package/dist/define/remote-function.d.ts +9 -14
  21. package/dist/define/remote-function.d.ts.map +1 -1
  22. package/dist/define/remote-function.js +9 -8
  23. package/dist/define/remote-function.js.map +1 -1
  24. package/dist/define/view.d.ts +8 -6
  25. package/dist/define/view.d.ts.map +1 -1
  26. package/dist/define/view.js +8 -6
  27. package/dist/define/view.js.map +1 -1
  28. package/dist/dispatch/identity.d.ts +10 -9
  29. package/dist/dispatch/identity.d.ts.map +1 -1
  30. package/dist/dispatch/identity.js +9 -15
  31. package/dist/dispatch/identity.js.map +1 -1
  32. package/dist/domain/binding.d.ts +18 -0
  33. package/dist/domain/binding.d.ts.map +1 -0
  34. package/dist/domain/binding.js +29 -0
  35. package/dist/domain/binding.js.map +1 -0
  36. package/dist/domain/build-spec.d.ts.map +1 -1
  37. package/dist/domain/build-spec.js +10 -7
  38. package/dist/domain/build-spec.js.map +1 -1
  39. package/dist/domain/define.d.ts +27 -27
  40. package/dist/domain/define.d.ts.map +1 -1
  41. package/dist/domain/define.js +31 -49
  42. package/dist/domain/define.js.map +1 -1
  43. package/dist/domain/extend-functions.d.ts +44 -0
  44. package/dist/domain/extend-functions.d.ts.map +1 -0
  45. package/dist/domain/extend-functions.js +69 -0
  46. package/dist/domain/extend-functions.js.map +1 -0
  47. package/dist/domain/extend-views.d.ts +45 -0
  48. package/dist/domain/extend-views.d.ts.map +1 -0
  49. package/dist/domain/extend-views.js +116 -0
  50. package/dist/domain/extend-views.js.map +1 -0
  51. package/dist/index.d.ts +2 -2
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +1 -1
  54. package/dist/index.js.map +1 -1
  55. package/dist/server/auxiliary-routes.js +1 -1
  56. package/dist/server/auxiliary-routes.js.map +1 -1
  57. package/dist/server/worker-entry.d.ts +3 -3
  58. package/dist/server/worker-entry.js +3 -3
  59. package/package.json +2 -5
  60. package/src/auth/index.ts +1 -1
  61. package/src/cli/run.ts +17 -12
  62. package/src/config/adapter.ts +9 -11
  63. package/src/config/define-domain.ts +34 -56
  64. package/src/config/deploy.ts +1 -1
  65. package/src/config/index.ts +1 -1
  66. package/src/define/remote-function.ts +9 -14
  67. package/src/define/view.ts +8 -6
  68. package/src/dispatch/identity.ts +15 -21
  69. package/src/domain/binding.ts +37 -0
  70. package/src/domain/build-spec.ts +18 -7
  71. package/src/domain/define.ts +67 -62
  72. package/src/domain/extend-functions.ts +86 -0
  73. package/src/domain/extend-views.ts +151 -0
  74. package/src/index.ts +7 -2
  75. package/src/server/auxiliary-routes.ts +1 -1
  76. package/src/server/worker-entry.ts +3 -3
  77. package/src/domain/extend-core.ts +0 -301
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Materialize the `functions` map (`defineRemoteFunction` entries) into the two
3
+ * halves a standalone-function DOMAIN MEMBER contributes — mirroring how a class
4
+ * method splits across compile + serialize:
5
+ *
6
+ * - `FunctionDeclarations` — the CONTRACT half, fed to `compileDomain`'s 4th
7
+ * arg. The member ref (`function.<slug>`), the layout
8
+ * (`/<origin>/functions/<slug>`), and the `of_domain` edge slug all derive
9
+ * from the map key; the kernel owns that mapping.
10
+ * - `FunctionSchema[]` + per-slug `FunctionBinding` — the IMPL/BINDING half,
11
+ * fed to `serialize` (url-stamped). The worker route is
12
+ * `<url>/<functionsFolder>/<slug>`, DECOUPLED from the graph layout — so
13
+ * `functionsFolder` only names the URL segment, not the node position.
14
+ *
15
+ * A standalone function is NOT a core/instance node: it is attached directly to
16
+ * the Domain via `of_domain` by the kernel-core serializer, so this module never
17
+ * touches `Core`.
18
+ */
19
+
20
+ import type { FunctionBinding } from '@astrale-os/kernel-api/routed'
21
+ import type { FunctionDeclarations, FunctionSchema } from '@astrale-os/kernel-core/domain'
22
+
23
+ import { zodToJsonSchema } from '@astrale-os/kernel-core/domain'
24
+
25
+ import type { AnyRemoteFunctionDef } from '../define/remote-function'
26
+
27
+ import { resolveBinding } from './binding'
28
+
29
+ /**
30
+ * Default URL segment for a function's worker route (`<url>/functions/<slug>`).
31
+ * Names the HTTP route only — the graph layout (`/<origin>/functions/<slug>`) is
32
+ * a fixed kernel convention, independent of this.
33
+ */
34
+ export const DEFAULT_FUNCTIONS_FOLDER = 'functions'
35
+ const SLUG_RE = /^[a-z][a-z0-9-]*$/
36
+
37
+ function assertValidSlugs(functions: Record<string, AnyRemoteFunctionDef>): void {
38
+ for (const slug of Object.keys(functions)) {
39
+ if (!SLUG_RE.test(slug)) {
40
+ throw new Error(
41
+ `defineRemoteDomain: invalid function slug "${slug}" — must match ${SLUG_RE}.`,
42
+ )
43
+ }
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Contract half — one declaration per `functions` map key, for `compileDomain`'s
49
+ * `functions` arg. The slug drives ref/path/of_domain; impl/binding arrive at
50
+ * serialize.
51
+ */
52
+ export function buildFunctionDeclarations(
53
+ functions: Record<string, AnyRemoteFunctionDef>,
54
+ ): FunctionDeclarations {
55
+ assertValidSlugs(functions)
56
+ const out: Record<string, { slug: string }> = {}
57
+ for (const slug of Object.keys(functions)) out[slug] = { slug }
58
+ return out
59
+ }
60
+
61
+ /**
62
+ * Impl/binding half — `FunctionSchema[]` for `serialize` plus the per-slug
63
+ * `FunctionBinding` map the worker mounts routes from. `ref` is the canonical
64
+ * `function.<slug>` member ref (matching `compiled.$.refs.functions`), so the
65
+ * serializer's per-member impl lookup finds it.
66
+ */
67
+ export function buildFunctionSchemas(
68
+ functions: Record<string, AnyRemoteFunctionDef>,
69
+ url: string,
70
+ functionsFolder: string,
71
+ ): { schemas: FunctionSchema[]; bindings: Record<string, FunctionBinding> } {
72
+ const schemas: FunctionSchema[] = []
73
+ const bindings: Record<string, FunctionBinding> = {}
74
+ for (const [slug, def] of Object.entries(functions)) {
75
+ const binding = resolveBinding(def.binding, url, functionsFolder, slug)
76
+ bindings[slug] = binding
77
+ schemas.push({
78
+ ref: `function.${slug}`,
79
+ inputSchema: zodToJsonSchema(def.inputSchema),
80
+ outputSchema: zodToJsonSchema(def.outputSchema),
81
+ output: 'value',
82
+ binding,
83
+ })
84
+ }
85
+ return { schemas, bindings }
86
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Materialize the `views` map (`defineView` entries) into the two halves a view
3
+ * DOMAIN MEMBER contributes — mirroring `extend-functions.ts`:
4
+ *
5
+ * - `ViewDeclarations` — the CONTRACT half, fed to `compileDomain`'s members
6
+ * arg. The member ref (`view.<slug>`), the layout (`/<origin>/views/<slug>`),
7
+ * and the `of_domain` edge slug all derive from the map key.
8
+ * - `ViewSchema[]` + per-slug `FunctionBinding` — the IMPL half, fed to
9
+ * `serialize`. A view carries more than a function: a `UI.handshake` mode and
10
+ * `view_for` target edges (resolved here to graph paths). The worker route is
11
+ * `<url>/<viewsFolder>/<slug>`, DECOUPLED from the graph layout.
12
+ *
13
+ * A view is NOT a core/instance node: the kernel-core serializer attaches it to
14
+ * the Domain via `of_domain` (slug `view.<slug>`) and emits its `view_for` edges,
15
+ * so this module never touches `Core`.
16
+ */
17
+
18
+ import type { FunctionBinding } from '@astrale-os/kernel-api/routed'
19
+ import type { ViewDeclarations, ViewSchema } from '@astrale-os/kernel-core/domain'
20
+ import type { Schema } from '@astrale-os/kernel-dsl'
21
+
22
+ import { DomainPaths } from '@astrale-os/kernel-core/domain'
23
+ import { buildDefOriginMap, scanImportedDefs } from '@astrale-os/kernel-core/domain'
24
+ import {
25
+ buildDefDescriptorMap,
26
+ isAbstract,
27
+ isCorePath,
28
+ isSelfMarker,
29
+ type EdgeEndpoint,
30
+ } from '@astrale-os/kernel-dsl'
31
+
32
+ import type { ViewDef } from '../define/view'
33
+
34
+ import { joinWorkerPath, resolveBinding } from './binding'
35
+
36
+ /**
37
+ * Default URL segment for a view's worker route (`<url>/views/<slug>`). Names the
38
+ * HTTP route only — the graph layout (`/<origin>/views/<slug>`) is a fixed kernel
39
+ * convention, independent of this.
40
+ */
41
+ export const DEFAULT_VIEWS_FOLDER = 'views'
42
+ const SLUG_RE = /^[a-z][a-z0-9-]*$/
43
+
44
+ // oxlint-disable-next-line no-explicit-any
45
+ type AnyViewDef = ViewDef<any>
46
+
47
+ function assertValidSlugs(views: Record<string, AnyViewDef>): void {
48
+ for (const slug of Object.keys(views)) {
49
+ if (!SLUG_RE.test(slug)) {
50
+ throw new Error(`defineRemoteDomain: invalid view slug "${slug}" — must match ${SLUG_RE}.`)
51
+ }
52
+ }
53
+ }
54
+
55
+ /** Enforce ViewDef's documented exclusivity: `mount` cannot combine with `render`/`binding`. */
56
+ function assertExclusivity(slug: string, def: AnyViewDef): void {
57
+ if (def.mount && (def.render || def.binding)) {
58
+ throw new Error(
59
+ `defineRemoteDomain: view "${slug}" sets \`mount\` together with ` +
60
+ `${def.render ? '`render`' : '`binding`'} — they are mutually exclusive.`,
61
+ )
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Contract half — one declaration per `views` map key, for `compileDomain`'s
67
+ * `views` arg. The slug drives ref/path/of_domain; impl/binding/view_for arrive
68
+ * at serialize.
69
+ */
70
+ export function buildViewDeclarations(views: Record<string, AnyViewDef>): ViewDeclarations {
71
+ assertValidSlugs(views)
72
+ const out: Record<string, { slug: string }> = {}
73
+ for (const slug of Object.keys(views)) out[slug] = { slug }
74
+ return out
75
+ }
76
+
77
+ /**
78
+ * Impl half — `ViewSchema[]` for `serialize` plus the per-slug `FunctionBinding`
79
+ * map the worker mounts routes from. `viewFor` markers are resolved here to
80
+ * concrete target paths (class meta-nodes) because the kernel-core serializer is
81
+ * pure and does not resolve `defineCore` endpoints.
82
+ */
83
+ export function buildViewSchemas(
84
+ views: Record<string, AnyViewDef>,
85
+ url: string,
86
+ viewsFolder: string,
87
+ schema: Schema,
88
+ ): { schemas: ViewSchema[]; bindings: Record<string, FunctionBinding> } {
89
+ const schemas: ViewSchema[] = []
90
+ const bindings: Record<string, FunctionBinding> = {}
91
+ for (const [slug, def] of Object.entries(views)) {
92
+ assertExclusivity(slug, def)
93
+ // `mount` is a worker-relative SPA path → `<url><mount>`; otherwise the
94
+ // default/overridden `<url>/<viewsFolder>/<slug>`.
95
+ const binding = def.mount
96
+ ? { remoteUrl: joinWorkerPath(url, def.mount) }
97
+ : resolveBinding(def.binding, url, viewsFolder, slug)
98
+ bindings[slug] = binding
99
+ // Inline `render` views ship no shell client → default handshake 'none'.
100
+ const handshake = def.handshake ?? (def.render ? 'none' : undefined)
101
+ schemas.push({
102
+ ref: `view.${slug}`,
103
+ name: slug,
104
+ binding,
105
+ ...(handshake ? { handshake } : {}),
106
+ viewFor: resolveViewForTargets(def.viewFor, schema),
107
+ })
108
+ }
109
+ return { schemas, bindings }
110
+ }
111
+
112
+ /**
113
+ * Resolve `defineView`'s `viewFor` endpoints to concrete graph path raws.
114
+ * Mirrors kernel-core `compileCore`'s endpoint resolution (the old channel that
115
+ * carried view_for as core edges): `selfOf(Def)` → the def's class/interface
116
+ * meta-node `/<declaringDomain>/{class,interface}.<Name>/self`; a `CorePath`
117
+ * string → its anchored graph path. Empty when the view targets nothing.
118
+ */
119
+ function resolveViewForTargets(
120
+ viewFor: EdgeEndpoint | readonly EdgeEndpoint[] | undefined,
121
+ schema: Schema,
122
+ ): string[] {
123
+ if (!viewFor) return []
124
+ const targets = Array.isArray(viewFor) ? viewFor : [viewFor]
125
+ if (targets.length === 0) return []
126
+
127
+ const descriptorMap = buildDefDescriptorMap(schema)
128
+ const defOrigins = buildDefOriginMap(schema, scanImportedDefs(schema))
129
+
130
+ return targets.map((target) => {
131
+ if (isSelfMarker(target)) {
132
+ const def = target.__def
133
+ const desc = descriptorMap.get(def)
134
+ if (!desc) {
135
+ throw new Error(
136
+ 'defineView: `viewFor: selfOf(...)` references a def that is neither in this ' +
137
+ "schema nor any of its `imports`. Add the def's schema to `imports`.",
138
+ )
139
+ }
140
+ const origin = defOrigins.get(def) ?? schema.domain
141
+ const dp = DomainPaths.of(origin)
142
+ return isAbstract(def) ? dp.interface(desc.name).raw : dp.class(desc.name).raw
143
+ }
144
+ if (isCorePath(target)) {
145
+ return DomainPaths.of(schema.domain).corePath(target).raw
146
+ }
147
+ throw new Error(
148
+ 'defineView: unsupported `viewFor` endpoint — use `selfOf(Class)` or a CorePath.',
149
+ )
150
+ })
151
+ }
package/src/index.ts CHANGED
@@ -19,7 +19,6 @@ export { remoteMethod, remoteClassMethods, remoteInterfaceMethods } from './meth
19
19
  // (preferred) use `domainWorkerEntry` from `@astrale-os/sdk/server`.
20
20
  export { defineDomain, deploy, defineAdapter } from './config'
21
21
  export type {
22
- ClientBinding,
23
22
  DefineDomainConfig,
24
23
  DomainDefinition,
25
24
  DeployConfig,
@@ -75,7 +74,13 @@ export type {
75
74
 
76
75
  // ─── Auth ────────────────────────────────────────────────────────────────
77
76
  export type { RemoteIdentityConfig, AuthenticateResult } from './auth'
78
- export { authenticateRequest, buildComposedGrant, signCredential } from './auth'
77
+ export {
78
+ authenticateRequest,
79
+ bindSelfKernel,
80
+ buildComposedGrant,
81
+ makeSelfKernel,
82
+ signCredential,
83
+ } from './auth'
79
84
  export { AuthMissingError, AuthInvalidError } from './auth'
80
85
  export { assertPerm, requireOwnership, READ, EDIT, USE, SHARE, ALL } from './auth'
81
86
  export type {
@@ -143,7 +143,7 @@ export function mountAuxiliaryRoutes<TDeps>(config: AuxiliaryRoutesConfig<TDeps>
143
143
  if (!outValidation.ok) {
144
144
  throw new SdkResultValidationError(
145
145
  outValidation.issues as SdkResultValidationError['issues'],
146
- def.ref,
146
+ `function.${slug}`,
147
147
  )
148
148
  }
149
149
  return c.json({ result: outValidation.data })
@@ -154,9 +154,9 @@ export function clientOrigin(url: URL, request: Request): string {
154
154
 
155
155
  /**
156
156
  * Build a `before` hook that serves a static-asset `binding` (e.g. a Workers
157
- * Assets binding) mounted under `base` (default `/ui`) — the runtime half of a
158
- * domain's `client` binding. Returns `undefined` for non-matching paths (and
159
- * when no binding is present) so the request falls through to domain dispatch.
157
+ * Assets binding) mounted under `base` (default `/ui`) — the runtime half of an
158
+ * adapter env's client-asset config. Returns `undefined` for non-matching paths
159
+ * (and when no binding is present) so the request falls through to domain dispatch.
160
160
  *
161
161
  * Asset URLs are rooted at `base` (a client bundler sets `base: '<base>/'`);
162
162
  * this hook strips that prefix before delegating to the binding, so
@@ -1,301 +0,0 @@
1
- /**
2
- * Build an augmented `Core<S>` that includes auto-materialized View and
3
- * standalone-function nodes (one per entry in `defineRemoteDomain`'s `views` /
4
- * `remoteFunctions` config), plus their parent Folder + optional `view_for`
5
- * edges. Returns the resolved effective bindings alongside so callers don't
6
- * recompute them.
7
- *
8
- * Standalone callables (the former `RemoteFunction` class) are materialized as
9
- * the canonical kernel `Function` node class — "remote" is just a `binding`,
10
- * not a distinct kind, so there is no per-domain class to configure.
11
- *
12
- * Conflicts: if the user core already declares a top-level node at
13
- * `<viewsFolder>` or `<functionsFolder>`, throws — the auto-materialization
14
- * reserves those slugs.
15
- */
16
-
17
- import type { FunctionBinding } from '@astrale-os/kernel-api/routed'
18
- import type {
19
- AnyEdgeDef,
20
- AnyNodeDef,
21
- Core,
22
- CoreEdgeEntry,
23
- CoreNodeEntry,
24
- Schema,
25
- } from '@astrale-os/kernel-dsl'
26
-
27
- import { Folder, K, KernelSchema } from '@astrale-os/kernel-core'
28
- import { buildCorePath } from '@astrale-os/kernel-dsl'
29
-
30
- /** Canonical node class for standalone callables (the former `RemoteFunction`). */
31
- const FUNCTION_CLASS = KernelSchema.classes.Function as unknown as AnyNodeDef
32
- /** Canonical node class for GUI views. */
33
- const VIEW_CLASS = KernelSchema.classes.View as unknown as AnyNodeDef
34
- /** Canonical edge class linking a View to its target. */
35
- const VIEW_FOR_EDGE_CLASS = KernelSchema.classes.view_for as unknown as AnyEdgeDef
36
-
37
- import type { AnyRemoteFunctionDef } from '../define/remote-function'
38
- import type { ViewDef } from '../define/view'
39
-
40
- export const DEFAULT_VIEWS_FOLDER = 'views'
41
- export const DEFAULT_FUNCTIONS_FOLDER = 'functions'
42
- const SLUG_RE = /^[a-z][a-z0-9-]*$/
43
-
44
- export type ExtendCoreConfig = {
45
- schema: Schema
46
- origin: string
47
- userCore?: Core
48
- /**
49
- * The worker's serving URL — the base every auto-materialized binding
50
- * resolves against. OMITTED at define time (the URL is known only to the
51
- * spec producers): the aux nodes then materialize structure-only (paths,
52
- * names, refs — what identity/subs resolution needs) with no `binding`
53
- * stamped and empty binding maps. Every install graph comes from a
54
- * `materializeRemoteDomain(domain, url)` call where it is present.
55
- */
56
- url?: string
57
-
58
- viewClass?: AnyNodeDef
59
- viewForEdgeClass?: AnyEdgeDef
60
- viewsFolder?: string
61
- // oxlint-disable-next-line no-explicit-any
62
- views?: Record<string, ViewDef<any>>
63
-
64
- functionsFolder?: string
65
- remoteFunctions?: Record<string, AnyRemoteFunctionDef>
66
- }
67
-
68
- export type ExtendCoreResult = {
69
- core: Core
70
- viewBindings: Record<string, FunctionBinding>
71
- remoteFunctionBindings: Record<string, FunctionBinding>
72
- }
73
-
74
- export function extendCore(config: ExtendCoreConfig): ExtendCoreResult {
75
- const {
76
- schema,
77
- origin,
78
- userCore,
79
- url,
80
- viewClass = VIEW_CLASS,
81
- viewForEdgeClass = VIEW_FOR_EDGE_CLASS,
82
- viewsFolder = DEFAULT_VIEWS_FOLDER,
83
- views,
84
- functionsFolder = DEFAULT_FUNCTIONS_FOLDER,
85
- remoteFunctions,
86
- } = config
87
-
88
- validateInputs(config)
89
-
90
- const nodes: CoreNodeEntry[] = userCore ? [...userCore.__nodes] : []
91
- const edges: CoreEdgeEntry[] = userCore ? [...userCore.__edges] : []
92
- const viewBindings: Record<string, FunctionBinding> = {}
93
- const remoteFunctionBindings: Record<string, FunctionBinding> = {}
94
- const enableUiHandshake = viewClass === VIEW_CLASS
95
-
96
- if (views && viewClass) {
97
- // Enforce ViewDef's documented exclusivity: `mount` (SPA route) cannot be
98
- // combined with `render` (inline HTML handler) or an explicit `binding` —
99
- // silently ignoring one of them would deploy a view that behaves
100
- // differently than its definition reads.
101
- for (const [slug, def] of Object.entries(views)) {
102
- if (def.mount && (def.render || def.binding)) {
103
- throw new Error(
104
- `defineRemoteDomain: view "${slug}" sets \`mount\` together with ` +
105
- `${def.render ? '`render`' : '`binding`'} — they are mutually exclusive.`,
106
- )
107
- }
108
- }
109
- assertNoConflict(nodes, origin, viewsFolder)
110
- addFolderAndEntries({
111
- origin,
112
- folderSlug: viewsFolder,
113
- nodes,
114
- entries: Object.entries(views).map(([slug, def]) => {
115
- const binding = url
116
- ? def.mount
117
- ? { remoteUrl: joinWorkerPath(url, def.mount) }
118
- : resolveBinding(def.binding, url, viewsFolder, slug)
119
- : undefined
120
- if (binding) viewBindings[slug] = binding
121
- return {
122
- slug,
123
- nodeClass: viewClass,
124
- data: buildViewData(slug, binding, def, enableUiHandshake),
125
- edges: buildViewForEdges(slug, def, viewForEdgeClass, viewsFolder, origin),
126
- }
127
- }),
128
- edges,
129
- })
130
- }
131
-
132
- if (remoteFunctions) {
133
- assertNoConflict(nodes, origin, functionsFolder)
134
- addFolderAndEntries({
135
- origin,
136
- folderSlug: functionsFolder,
137
- nodes,
138
- entries: Object.entries(remoteFunctions).map(([slug, def]) => {
139
- const binding = url ? resolveBinding(def.binding, url, functionsFolder, slug) : undefined
140
- if (binding) remoteFunctionBindings[slug] = binding
141
- return {
142
- slug,
143
- // Standalone callables materialize as the canonical kernel Function class.
144
- nodeClass: FUNCTION_CLASS,
145
- data: {
146
- ...buildFunctionData(slug, binding),
147
- [K.$.i('Function').ref.key]: def.ref ?? `function.${slug}`,
148
- },
149
- edges: [],
150
- }
151
- }),
152
- edges,
153
- })
154
- }
155
-
156
- return {
157
- core: { schema, domain: origin, __nodes: nodes, __edges: edges },
158
- viewBindings,
159
- remoteFunctionBindings,
160
- }
161
- }
162
-
163
- /** Join a worker-relative mount path onto the serving url (single slash). */
164
- function joinWorkerPath(url: string, mount: string): string {
165
- const base = url.replace(/\/+$/, '')
166
- return `${base}/${mount.replace(/^\/+/, '')}`
167
- }
168
-
169
- export function resolveBinding(
170
- override: FunctionBinding | undefined,
171
- url: string,
172
- folderSlug: string,
173
- slug: string,
174
- ): FunctionBinding {
175
- // Same trailing-slash discipline as `joinWorkerPath` — a `url` ending in `/`
176
- // must not produce `//` in the binding (the kernel pins iss by exact string).
177
- const base = url.replace(/\/+$/, '')
178
- if (override) {
179
- return {
180
- ...override,
181
- remoteUrl: override.remoteUrl ?? `${base}/${folderSlug}/${slug}`,
182
- }
183
- }
184
- return { remoteUrl: `${base}/${folderSlug}/${slug}` }
185
- }
186
-
187
- // ── Internal ───────────────────────────────────────────────────────────────
188
-
189
- function validateInputs(config: ExtendCoreConfig): void {
190
- if (config.views) {
191
- for (const slug of Object.keys(config.views)) {
192
- if (!SLUG_RE.test(slug)) {
193
- throw new Error(`defineRemoteDomain: invalid view slug "${slug}" — must match ${SLUG_RE}.`)
194
- }
195
- }
196
- }
197
- if (config.remoteFunctions) {
198
- for (const slug of Object.keys(config.remoteFunctions)) {
199
- if (!SLUG_RE.test(slug)) {
200
- throw new Error(
201
- `defineRemoteDomain: invalid remote-function slug "${slug}" — must match ${SLUG_RE}.`,
202
- )
203
- }
204
- }
205
- }
206
- }
207
-
208
- function assertNoConflict(
209
- nodes: readonly CoreNodeEntry[],
210
- origin: string,
211
- folderSlug: string,
212
- ): void {
213
- const folderPath = buildCorePath(origin, [folderSlug])
214
- if (nodes.some((n) => n.path === folderPath)) {
215
- throw new Error(
216
- `defineRemoteDomain: top-level core slug "${folderSlug}" is reserved by ` +
217
- 'SDK auto-materialization. Move the conflicting node or rename the ' +
218
- '`viewsFolder` / `functionsFolder` config.',
219
- )
220
- }
221
- }
222
-
223
- type EntryDescriptor = {
224
- slug: string
225
- nodeClass: AnyNodeDef
226
- data: Record<string, unknown>
227
- edges: CoreEdgeEntry[]
228
- }
229
-
230
- function addFolderAndEntries(args: {
231
- origin: string
232
- folderSlug: string
233
- entries: EntryDescriptor[]
234
- nodes: CoreNodeEntry[]
235
- edges: CoreEdgeEntry[]
236
- }): void {
237
- const { origin, folderSlug, entries, nodes, edges } = args
238
-
239
- const folderPath = buildCorePath(origin, [folderSlug])
240
- nodes.push({
241
- path: folderPath,
242
- def: Folder as unknown as AnyNodeDef,
243
- data: { [K.Named.name.key]: folderSlug },
244
- })
245
-
246
- for (const entry of entries) {
247
- nodes.push({
248
- path: buildCorePath(origin, [folderSlug, entry.slug]),
249
- def: entry.nodeClass,
250
- data: entry.data,
251
- parent: folderPath,
252
- })
253
- for (const e of entry.edges) edges.push(e)
254
- }
255
- }
256
-
257
- function buildFunctionData(
258
- slug: string,
259
- binding: FunctionBinding | undefined,
260
- ): Record<string, unknown> {
261
- // Match the kernel-core schema serializer which JSON-stringifies the
262
- // `Function.binding` value on Function nodes (see
263
- // kernel/core/domain/serialize/schema.ts). The kernel install validator
264
- // checks `typeof binding === 'string'` then parses; storing a raw object
265
- // trips the validator with "missing remote binding" even when the object IS
266
- // the binding. No binding at all (define-time, no url) → no prop stamped.
267
- return {
268
- [K.Named.name.key]: slug,
269
- ...(binding ? { [K.$.i('Function').binding.key]: JSON.stringify(binding) } : {}),
270
- }
271
- }
272
-
273
- function buildViewData(
274
- slug: string,
275
- binding: FunctionBinding | undefined,
276
- def: ViewDef,
277
- enableUiHandshake: boolean,
278
- ): Record<string, unknown> {
279
- const handshake = def.handshake ?? (def.render ? 'none' : undefined)
280
- return {
281
- ...buildFunctionData(slug, binding),
282
- ...(enableUiHandshake && handshake ? { [K.UI.handshake.key]: handshake } : {}),
283
- }
284
- }
285
-
286
- function buildViewForEdges(
287
- slug: string,
288
- def: ViewDef,
289
- edgeClass: AnyEdgeDef | undefined,
290
- viewsFolder: string,
291
- origin: string,
292
- ): CoreEdgeEntry[] {
293
- if (!def.viewFor || !edgeClass) return []
294
- const targets = Array.isArray(def.viewFor) ? def.viewFor : [def.viewFor]
295
- const from = buildCorePath(origin, [viewsFolder, slug])
296
- return targets.map((target) => ({
297
- from,
298
- edge: edgeClass,
299
- to: target as CoreEdgeEntry['to'],
300
- }))
301
- }