@astrale-os/sdk 0.1.5 → 0.1.7

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 (108) hide show
  1. package/dist/auth/verify.d.ts +2 -0
  2. package/dist/auth/verify.d.ts.map +1 -1
  3. package/dist/auth/verify.js +81 -26
  4. package/dist/auth/verify.js.map +1 -1
  5. package/dist/cli/bin.d.ts +7 -0
  6. package/dist/cli/bin.d.ts.map +1 -0
  7. package/dist/cli/bin.js +15 -0
  8. package/dist/cli/bin.js.map +1 -0
  9. package/dist/cli/dotenv.d.ts +13 -0
  10. package/dist/cli/dotenv.d.ts.map +1 -0
  11. package/dist/cli/dotenv.js +46 -0
  12. package/dist/cli/dotenv.js.map +1 -0
  13. package/dist/cli/index.d.ts +15 -0
  14. package/dist/cli/index.d.ts.map +1 -0
  15. package/dist/cli/index.js +15 -0
  16. package/dist/cli/index.js.map +1 -0
  17. package/dist/cli/run.d.ts +79 -0
  18. package/dist/cli/run.d.ts.map +1 -0
  19. package/dist/cli/run.js +569 -0
  20. package/dist/cli/run.js.map +1 -0
  21. package/dist/cli/spec.d.ts +19 -0
  22. package/dist/cli/spec.d.ts.map +1 -0
  23. package/dist/cli/spec.js +31 -0
  24. package/dist/cli/spec.js.map +1 -0
  25. package/dist/config/adapter.d.ts +140 -0
  26. package/dist/config/adapter.d.ts.map +1 -0
  27. package/dist/config/adapter.js +40 -0
  28. package/dist/config/adapter.js.map +1 -0
  29. package/dist/config/define-domain.d.ts +112 -0
  30. package/dist/config/define-domain.d.ts.map +1 -0
  31. package/dist/config/define-domain.js +98 -0
  32. package/dist/config/define-domain.js.map +1 -0
  33. package/dist/config/deploy.d.ts +28 -0
  34. package/dist/config/deploy.d.ts.map +1 -0
  35. package/dist/config/deploy.js +24 -0
  36. package/dist/config/deploy.js.map +1 -0
  37. package/dist/config/index.d.ts +21 -0
  38. package/dist/config/index.d.ts.map +1 -0
  39. package/dist/config/index.js +18 -0
  40. package/dist/config/index.js.map +1 -0
  41. package/dist/define/remote-function.d.ts +19 -11
  42. package/dist/define/remote-function.d.ts.map +1 -1
  43. package/dist/define/remote-function.js.map +1 -1
  44. package/dist/dispatch/call-remote.d.ts +7 -3
  45. package/dist/dispatch/call-remote.d.ts.map +1 -1
  46. package/dist/dispatch/call-remote.js.map +1 -1
  47. package/dist/dispatch/dispatcher.d.ts.map +1 -1
  48. package/dist/dispatch/dispatcher.js +8 -4
  49. package/dist/dispatch/dispatcher.js.map +1 -1
  50. package/dist/dispatch/index.d.ts +1 -1
  51. package/dist/dispatch/index.d.ts.map +1 -1
  52. package/dist/dispatch/index.js.map +1 -1
  53. package/dist/dispatch/self.d.ts +46 -10
  54. package/dist/dispatch/self.d.ts.map +1 -1
  55. package/dist/dispatch/self.js +65 -8
  56. package/dist/dispatch/self.js.map +1 -1
  57. package/dist/domain/define.d.ts +3 -3
  58. package/dist/domain/define.js +3 -3
  59. package/dist/index.d.ts +5 -4
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +8 -2
  62. package/dist/index.js.map +1 -1
  63. package/dist/method/class.d.ts.map +1 -1
  64. package/dist/method/class.js.map +1 -1
  65. package/dist/method/context.d.ts +32 -7
  66. package/dist/method/context.d.ts.map +1 -1
  67. package/dist/method/index.d.ts +1 -1
  68. package/dist/method/index.d.ts.map +1 -1
  69. package/dist/method/single.d.ts +16 -11
  70. package/dist/method/single.d.ts.map +1 -1
  71. package/dist/method/single.js.map +1 -1
  72. package/dist/server/domain-entry.d.ts +67 -0
  73. package/dist/server/domain-entry.d.ts.map +1 -0
  74. package/dist/server/domain-entry.js +58 -0
  75. package/dist/server/domain-entry.js.map +1 -0
  76. package/dist/server/index.d.ts +3 -1
  77. package/dist/server/index.d.ts.map +1 -1
  78. package/dist/server/index.js +2 -1
  79. package/dist/server/index.js.map +1 -1
  80. package/dist/server/worker-entry.d.ts +57 -5
  81. package/dist/server/worker-entry.d.ts.map +1 -1
  82. package/dist/server/worker-entry.js +108 -24
  83. package/dist/server/worker-entry.js.map +1 -1
  84. package/package.json +12 -3
  85. package/src/auth/verify.ts +89 -28
  86. package/src/cli/bin.ts +15 -0
  87. package/src/cli/dotenv.ts +45 -0
  88. package/src/cli/index.ts +15 -0
  89. package/src/cli/run.ts +675 -0
  90. package/src/cli/spec.ts +42 -0
  91. package/src/config/adapter.ts +172 -0
  92. package/src/config/define-domain.ts +218 -0
  93. package/src/config/deploy.ts +35 -0
  94. package/src/config/index.ts +31 -0
  95. package/src/define/remote-function.ts +42 -13
  96. package/src/dispatch/call-remote.ts +7 -2
  97. package/src/dispatch/dispatcher.ts +8 -4
  98. package/src/dispatch/index.ts +1 -1
  99. package/src/dispatch/self.ts +96 -10
  100. package/src/domain/define.ts +3 -3
  101. package/src/index.ts +25 -4
  102. package/src/method/class.ts +4 -3
  103. package/src/method/context.ts +38 -7
  104. package/src/method/index.ts +1 -1
  105. package/src/method/single.ts +30 -11
  106. package/src/server/domain-entry.ts +113 -0
  107. package/src/server/index.ts +3 -1
  108. package/src/server/worker-entry.ts +122 -23
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Build a diagnostic spec (wire graph + schema hash) from the project's
3
+ * `defineDomain` definition. This mirrors what the codegen'd worker assembles —
4
+ * but only for inspection / `/meta` provenance. The live install bundle is
5
+ * always produced by the worker at request time, so a failure here is non-fatal
6
+ * for dev/deploy (the CLI logs and continues) and fatal only for
7
+ * `astrale-domain build`, where the spec IS the product.
8
+ *
9
+ * The modules (`schema` / `methods` / `views` / `functions`) come straight off
10
+ * the definition — the SAME values the author wired into `defineDomain`, which
11
+ * the adapter codegen also assembles. One source, no second filesystem probe to
12
+ * drift from it.
13
+ */
14
+
15
+ import type { Schema } from '@astrale-os/kernel-dsl'
16
+
17
+ import { hashInstallGraph } from '@astrale-os/kernel-core/domain'
18
+
19
+ import type { DomainDefinition } from '../config/define-domain'
20
+ import type { RemoteDomainConfig } from '../domain'
21
+
22
+ import { buildInstallGraph, defineRemoteDomain } from '../domain'
23
+
24
+ export async function buildProjectSpec(
25
+ def: DomainDefinition,
26
+ url: string,
27
+ ): Promise<{ wire: Record<string, unknown>; schemaHash: string }> {
28
+ // The definition's module values are untyped against `RemoteDomainConfig` by
29
+ // nature (deps/schema generics erased on `DomainDefinition`); funnel them
30
+ // through ONE cast into the config shape `defineRemoteDomain` validates.
31
+ const config = {
32
+ schema: def.schema,
33
+ methods: def.methods,
34
+ ...(def.views ? { views: def.views } : {}),
35
+ ...(def.functions ? { remoteFunctions: def.functions } : {}),
36
+ } as RemoteDomainConfig<Schema, unknown>
37
+
38
+ const domain = defineRemoteDomain<unknown>()(config)
39
+ const wire = buildInstallGraph(domain, url)
40
+ const schemaHash = await hashInstallGraph(wire)
41
+ return { wire: wire as unknown as Record<string, unknown>, schemaHash }
42
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * The deployment-adapter contract.
3
+ *
4
+ * A deployment target = one npm package implementing `DomainAdapter<Params>`.
5
+ * The adapter is GENERIC over its own `Params` so envs AND provider settings
6
+ * stay 100% adapter-specific (a Cloudflare adapter's `{ route, secrets }` is
7
+ * nothing like a Node adapter's `{ port }`). The CLI resolves
8
+ * `envs[<env>] → Params` via `adapter.params(env)` and then drives the adapter
9
+ * with the resolved params — install of the resulting URL and the `requires`
10
+ * check are generic and live in the CLI, never in the adapter.
11
+ *
12
+ * The central guarantee: `watch()` and `deploy()` both return a `url` — that's
13
+ * what the CLI prints, what `astrale domain install <url>` consumes, and
14
+ * the worker's `iss` identity. The URL is an OUTPUT of watch/deploy; nothing
15
+ * in the contract asks an adapter to predict it ahead of time.
16
+ */
17
+
18
+ /**
19
+ * Generic, target-independent domain metadata the CLI hands every adapter so
20
+ * it can codegen the worker entry. (Extends the bare `WatchCtx`/`DeployCtx` of
21
+ * the spec with the fields a real codegen needs.)
22
+ */
23
+ export interface DomainInfo {
24
+ /**
25
+ * The domain's addressing name — the graph slug it mounts under (e.g.
26
+ * `'crm.acme.dev'`). NOT the cryptographic identity: the JWT `iss` is the
27
+ * worker's serving URL, pinned by the kernel at install time.
28
+ */
29
+ origin: string
30
+ /** Cross-domain deps by origin — verified at install (CLI/kernel), not here. */
31
+ requires: readonly string[]
32
+ /** Optional Astrale Path called by the kernel after install (as __SYSTEM__). */
33
+ postInstall?: string
34
+ /**
35
+ * Whether the domain declares any Views / standalone Functions / a client SPA
36
+ * — read from the `defineDomain` definition, NOT probed from the filesystem.
37
+ * The adapter codegen imports the corresponding module / mounts the SPA hook
38
+ * only when true. `hasClient` governs the WORKER's `/ui` hook specifically
39
+ * (present whenever the domain has a client, even on managed deploys that ship
40
+ * the built assets separately and pass no `clientDir`); the wrangler
41
+ * `assets.directory` binding tracks `clientDir` instead.
42
+ */
43
+ hasViews: boolean
44
+ hasFunctions: boolean
45
+ hasClient: boolean
46
+ /**
47
+ * Whether the domain declares a `deps` mapper (`defineDomain({ deps })`).
48
+ * When true the adapter codegen imports it from the domain's fixed `deps`
49
+ * module and passes it to the worker entry; when false the worker passes the
50
+ * raw env straight through as `ctx.deps`. Read from the definition, never
51
+ * probed from the filesystem.
52
+ */
53
+ hasDeps: boolean
54
+ }
55
+
56
+ export interface WatchCtx {
57
+ /** Absolute path to the domain project root. */
58
+ projectDir: string
59
+ /** Absolute path where the CLI writes the diagnostic `spec.json`. */
60
+ specPath: string
61
+ /** Absolute path to the client SPA dir, when the domain declares a `client`. */
62
+ clientDir?: string
63
+ /** Flat secret map loaded from the env's `secrets` file — injected locally in dev. */
64
+ secrets: Record<string, string>
65
+ /** Domain metadata for codegen. */
66
+ domain: DomainInfo
67
+ /** The env key being watched (e.g. 'dev'). */
68
+ env: string
69
+ /** Invoked by the adapter on every hot-reload so the CLI can re-log. */
70
+ onReload(): void
71
+ }
72
+
73
+ export interface DeployCtx {
74
+ projectDir: string
75
+ specPath: string
76
+ /** Flat secret map loaded from the env's `secrets` file (gitignored). */
77
+ secrets: Record<string, string>
78
+ clientDir?: string
79
+ domain: DomainInfo
80
+ /** The env key being deployed (e.g. 'dev' | 'prod' | 'canary'). */
81
+ env: string
82
+ }
83
+
84
+ /** A running local watch — exposes its URL and a stop handle. */
85
+ export interface WatchHandle {
86
+ /** Local URL the worker is reachable at (e.g. http://localhost:8787). */
87
+ url: string
88
+ stop(): Promise<void>
89
+ }
90
+
91
+ /** A completed deploy — the authoritative public URL. */
92
+ export interface DeployResult {
93
+ url: string
94
+ /**
95
+ * Adapter-specific next-steps block printed INSTEAD of the CLI's default
96
+ * "install on an instance" footer — managed deploys already installed the
97
+ * domain, so the default hint is wrong there. Pre-indented plain lines.
98
+ */
99
+ nextSteps?: string
100
+ }
101
+
102
+ export interface DomainAdapter<Params> {
103
+ /** 'astrale-host' | 'cloudflare' | 'node' | … */
104
+ name: string
105
+
106
+ /** Resolve an env key to this adapter's typed params. Throws on unknown key. */
107
+ params(env: string): Params
108
+
109
+ /** Local + hot-reload (watch) → controllable handle. Orthogonal to env. */
110
+ watch(params: Params, ctx: WatchCtx): Promise<WatchHandle>
111
+
112
+ /**
113
+ * Optional: re-run codegen for an ALREADY-RUNNING `watch` after
114
+ * `astrale.config.ts` changed — same shape as `watch` but it must NOT spawn
115
+ * anything: it only rewrites the generated files, and the running dev server
116
+ * (which watches them, e.g. `wrangler dev` on its `--config`) picks them up.
117
+ * Adapters without it fall back to "restart to apply config changes".
118
+ */
119
+ regenerate?(params: Params, ctx: WatchCtx): Promise<void>
120
+
121
+ /** Build + bundle + ship (the adapter owns ITS build) → authoritative URL. */
122
+ deploy(params: Params, ctx: DeployCtx): Promise<DeployResult>
123
+
124
+ /**
125
+ * Optional: the path (relative to the project root) of the gitignored secrets
126
+ * file for these params. The CLI loads it and passes the parsed map as
127
+ * `ctx.secrets` — keeping secret-file loading generic while the path stays
128
+ * provider-typed. Return `undefined` for "no secrets file".
129
+ */
130
+ secretsFile?(params: Params): string | undefined
131
+ }
132
+
133
+ /**
134
+ * Spec passed to `defineAdapter` — identical to `DomainAdapter` minus the
135
+ * `params(env)` resolver, which the helper attaches from `envs`. Custom adapter
136
+ * authors use this so they never re-implement env→params resolution.
137
+ */
138
+ export interface AdapterSpec<Params> {
139
+ name: string
140
+ /** The provider-typed env map (`{ dev: {...}, prod: {...}, … }`). */
141
+ envs: Record<string, Params>
142
+ watch(params: Params, ctx: WatchCtx): Promise<WatchHandle>
143
+ regenerate?(params: Params, ctx: WatchCtx): Promise<void>
144
+ deploy(params: Params, ctx: DeployCtx): Promise<DeployResult>
145
+ secretsFile?(params: Params): string | undefined
146
+ }
147
+
148
+ /**
149
+ * Build a `DomainAdapter` from a spec + an env map, attaching a `params(env)`
150
+ * resolver that throws a clear error on an unknown key (listing the valid ones).
151
+ */
152
+ export function defineAdapter<Params>(spec: AdapterSpec<Params>): DomainAdapter<Params> {
153
+ const keys = Object.keys(spec.envs)
154
+ return {
155
+ name: spec.name,
156
+ params(env: string): Params {
157
+ const params = spec.envs[env]
158
+ if (params === undefined) {
159
+ const known = keys.length ? keys.join(', ') : '(none)'
160
+ throw new Error(
161
+ `Adapter "${spec.name}": unknown env "${env}". Known envs: ${known}. ` +
162
+ `Add it to the adapter's env map in astrale.config.ts.`,
163
+ )
164
+ }
165
+ return params
166
+ },
167
+ watch: spec.watch,
168
+ deploy: spec.deploy,
169
+ ...(spec.regenerate ? { regenerate: spec.regenerate } : {}),
170
+ ...(spec.secretsFile ? { secretsFile: spec.secretsFile } : {}),
171
+ }
172
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * `defineDomain` — the WORKER-SAFE definition of a domain: what the domain *is*
3
+ * (its `schema`, `methods`, `deps`, `views`, standalone `functions`, `client`
4
+ * SPA) plus its addressing identity (`origin`, `requires`, `postInstall`). It
5
+ * deliberately carries NO deployment adapter — the adapter (`cloudflare(...)`,
6
+ * `astrale(...)`) is node-only code (filesystem, wrangler) that must never enter
7
+ * the worker bundle. The author wires this in a `domain.ts` the generated worker
8
+ * imports directly, then attaches the adapter separately with `deploy(domain,
9
+ * adapter)` in `astrale.config.ts` (see `./deploy`).
10
+ *
11
+ * The modules are wired EXPLICITLY here — imported and passed in — not
12
+ * discovered from magic folder names. A renamed or mistyped module is a compile
13
+ * error at this call site, never a silently-missing worker route. The adapter
14
+ * reads this one definition for everything it codegens; there is no second
15
+ * filesystem probe to drift from it. `defineDomain` itself builds no server and
16
+ * boots no kernel — it validates and packages the declaration.
17
+ */
18
+
19
+ import type { Schema } from '@astrale-os/kernel-dsl'
20
+
21
+ import { DomainOrigin, extractDomainSlug } from '@astrale-os/kernel-core/domain'
22
+
23
+ import type { RemoteFunctionDef, ViewDef } from '../define'
24
+ import type { SchemaMethodsImpl } from '../method'
25
+
26
+ // A registry holds views / functions of every deps + param/result shape. Their
27
+ // per-entry typing is enforced at the `defineView` / `defineRemoteFunction` call
28
+ // site; here they're held loosely and, crucially, kept OUT of `TDeps` inference
29
+ // — so `TDeps` is fixed by `methods` alone (the authoritative deps source) and a
30
+ // view authored with the default `unknown` deps can't fight `methods`'s `Env`.
31
+ // oxlint-disable no-explicit-any
32
+ type AnyViewDef = ViewDef<any>
33
+ type AnyFunctionDef = RemoteFunctionDef<any, any, any>
34
+ // oxlint-enable no-explicit-any
35
+
36
+ /**
37
+ * The domain's client SPA binding. Its presence is the whole signal — there is
38
+ * no `existsSync('client/')` probe. `dir` is the project-relative source folder
39
+ * (e.g. `'client'`); the worker serves its built SPA under `/ui` via its Assets
40
+ * binding. Always written out explicitly (`client: { dir: 'client' }`) — there
41
+ * is no boolean shorthand, so the source folder is never implicit.
42
+ */
43
+ export type ClientBinding = { dir: string }
44
+
45
+ export interface DefineDomainConfig<S extends Schema, TDeps, TEnv = unknown> {
46
+ /** The domain schema (from `schema/`). Its `.domain` seeds the default origin. */
47
+ schema: S
48
+ /**
49
+ * The domain's method implementations (from `methods/`), one per schema
50
+ * method. Typed against `schema` — an unimplemented or misnamed method is a
51
+ * compile error here.
52
+ */
53
+ methods: SchemaMethodsImpl<S, TDeps>
54
+ /**
55
+ * Map the worker `env` to the handler dependency container (`ctx.deps`).
56
+ * Run ONCE per cold isolate per serving URL (the built app is cached), NOT
57
+ * per request — so it's the place to construct ports/clients once instead of
58
+ * re-deriving them in every handler. Its return type IS `TDeps`: type it the
59
+ * shape your `methods` read (e.g. `(env) => ({ platform: buildPlatform(env) })`)
60
+ * and the methods get the rich container, not raw env. The same value reaches
61
+ * view/function handlers and the `install.authorize` hook.
62
+ *
63
+ * Omit for the common case: `env` is passed straight through (`TDeps = TEnv`),
64
+ * so existing domains are unaffected. The function itself is imported by the
65
+ * generated worker from a fixed `deps` module, mirroring `methods` — wire it
66
+ * here so the type check binds `env → TDeps` and the adapter knows to emit it.
67
+ */
68
+ deps?: (env: TEnv, url: string) => TDeps
69
+ /**
70
+ * The domain's Views (iframe-mountable UIs), keyed by slug. Omit when the
71
+ * domain has none. Each becomes a `View` node + a worker route.
72
+ */
73
+ views?: Record<string, AnyViewDef>
74
+ /**
75
+ * The domain's standalone Functions (callables not bound to a class), keyed
76
+ * by slug. Omit when the domain has none.
77
+ */
78
+ functions?: Record<string, AnyFunctionDef>
79
+ /**
80
+ * The domain's client SPA, e.g. `{ dir: 'client' }`. Its presence enables it
81
+ * (no folder probing); `dir` is the project-relative source folder, built and
82
+ * served under `/ui`. Omit for a domain with no SPA.
83
+ */
84
+ client?: ClientBinding
85
+ /**
86
+ * The domain's **addressing name** (the graph slug it mounts under, e.g.
87
+ * `'crm.acme.dev'`). Defaults to `schema.domain`. Must be a name, never a
88
+ * URL. This is **NOT** the cryptographic identity: the `iss` is the worker's
89
+ * **serving URL**, pinned by the kernel to the URL it fetched the domain at
90
+ * during install (and verified against that URL's JWKS). `origin` is a free
91
+ * addressing label, only required to be unique in the graph.
92
+ */
93
+ origin?: string
94
+ /** Cross-domain deps, by origin. Verified present on the instance at install. */
95
+ requires?: readonly string[]
96
+ /**
97
+ * Astrale Path (usually an AbsolutePath to a `functions/` entry) the kernel
98
+ * calls once after install, as __SYSTEM__ — where the domain posts its own
99
+ * grants / seed.
100
+ */
101
+ postInstall?: string
102
+ }
103
+
104
+ export interface DomainDefinition {
105
+ schema: Schema
106
+ // oxlint-disable-next-line no-explicit-any -- erased at the consumption casts in spec/codegen
107
+ methods: SchemaMethodsImpl<Schema, any>
108
+ /**
109
+ * env → deps mapper, when the author supplied one. Held loosely (the worker
110
+ * imports the real function from its own `deps` module); the CLI reads only
111
+ * its PRESENCE to set `DomainInfo.hasDeps`, the codegen signal.
112
+ */
113
+ // oxlint-disable-next-line no-explicit-any -- presence is what the CLI consumes; the typed binding lives at the call site
114
+ deps?: (env: any, url: string) => any
115
+ views?: Record<string, ViewDef>
116
+ functions?: Record<string, AnyFunctionDef>
117
+ /** Normalized client binding (resolved `dir`), or absent when the domain has no SPA. */
118
+ client?: { dir: string }
119
+ origin: string
120
+ requires: readonly string[]
121
+ postInstall?: string
122
+ }
123
+
124
+ export function defineDomain<S extends Schema, TDeps, TEnv = unknown>(
125
+ config: DefineDomainConfig<S, TDeps, TEnv>,
126
+ ): DomainDefinition {
127
+ const rawOrigin = config.origin ?? schemaDomain(config.schema)
128
+ if (!rawOrigin) {
129
+ throw new Error(
130
+ 'defineDomain: could not resolve `origin`. Set it explicitly or give the ' +
131
+ 'schema a base domain (`defineSchema("crm.acme.dev", …)`).',
132
+ )
133
+ }
134
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(rawOrigin)) {
135
+ throw new Error(
136
+ `defineDomain: \`origin\` must be a stable name (e.g. "crm.acme.dev"), not a URL — got "${rawOrigin}". ` +
137
+ 'The deployment URL is an output of deploy, not the identity.',
138
+ )
139
+ }
140
+ // Canonicalize + validate the origin exactly as the kernel does (lowercased,
141
+ // FQDN-like, matches DOMAIN_ORIGIN_RE). Failing HERE gives a clear authoring-
142
+ // time error instead of a cryptic failure deep in codegen / install, and makes
143
+ // `origin` the same lowercased form the kernel stores graph nodes under.
144
+ let origin: string
145
+ try {
146
+ origin = DomainOrigin(rawOrigin)
147
+ } catch {
148
+ throw new Error(
149
+ `defineDomain: invalid \`origin\` "${rawOrigin}". Use a lowercase FQDN-like slug ` +
150
+ '(letters, digits, ".", "_", "-"), e.g. "crm.acme.dev".',
151
+ )
152
+ }
153
+
154
+ // `requires` entries are domain origins — validate them with the same rule as
155
+ // `origin` so a URL or mixed-case entry fails at authoring time, not install.
156
+ const requires = (config.requires ?? []).map((dep) => {
157
+ try {
158
+ return DomainOrigin(dep)
159
+ } catch {
160
+ throw new Error(
161
+ `defineDomain: invalid \`requires\` entry "${dep}". Use the dependency's origin slug ` +
162
+ '(lowercase FQDN-like, e.g. "dist.astrale.ai"), not a URL.',
163
+ )
164
+ }
165
+ })
166
+
167
+ return {
168
+ schema: config.schema,
169
+ methods: config.methods as DomainDefinition['methods'],
170
+ ...(config.deps ? { deps: config.deps as DomainDefinition['deps'] } : {}),
171
+ ...(config.views ? { views: config.views as Record<string, ViewDef> } : {}),
172
+ ...(config.functions ? { functions: config.functions } : {}),
173
+ ...(config.client ? { client: config.client } : {}),
174
+ origin,
175
+ requires,
176
+ ...(config.postInstall
177
+ ? { postInstall: normalizePostInstall(config.postInstall, origin) }
178
+ : {}),
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Validate a `postInstall` hook path and align its leading origin segment with
184
+ * the canonical (lowercased) origin. Only the typed colon forms are accepted
185
+ * (`/:<origin>:class.X:seed`, `/:<origin>:interface.Ops:seed`) — mirroring the
186
+ * kernel's own origin guard, which refuses absolute tree paths because they
187
+ * cannot prove their origin from the string alone. The hook is often authored
188
+ * as `` `/:${schema.domain}:…` `` where `schema.domain` keeps its source
189
+ * casing, but the kernel stores graph nodes (and runs its guard) under the
190
+ * lowercased origin — so the origin segment is re-stamped canonical. Rejects a
191
+ * tree path or one pointing at a different domain (the kernel calls the hook
192
+ * as __SYSTEM__ and refuses a foreign target).
193
+ */
194
+ function normalizePostInstall(postInstall: string, origin: string): string {
195
+ const slug = extractDomainSlug(postInstall)
196
+ if (slug === null) {
197
+ throw new Error(
198
+ `defineDomain: \`postInstall\` must be a typed colon-path under "/:${origin}" ` +
199
+ `(e.g. "/:${origin}:class.Note:seed" or "/:${origin}:interface.Ops:seed"); ` +
200
+ `absolute tree paths are not accepted — got "${postInstall}".`,
201
+ )
202
+ }
203
+ if (slug.toLowerCase() !== origin) {
204
+ throw new Error(
205
+ `defineDomain: \`postInstall\` "${postInstall}" must resolve under the domain origin ` +
206
+ `"/${origin}" — the kernel calls it as __SYSTEM__ and refuses a hook pointing at another domain.`,
207
+ )
208
+ }
209
+ // Re-stamp the origin segment with its canonical (lowercased) form.
210
+ return postInstall.startsWith('/:')
211
+ ? `/:${origin}${postInstall.slice(2 + slug.length)}`
212
+ : `/${origin}${postInstall.slice(1 + slug.length)}`
213
+ }
214
+
215
+ function schemaDomain(schema: Schema): string | undefined {
216
+ const value = (schema as unknown as { domain?: unknown }).domain
217
+ return typeof value === 'string' && value.length > 0 ? value : undefined
218
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `deploy` — bind a worker-safe `DomainDefinition` to a deployment adapter,
3
+ * producing the value `astrale.config.ts` default-exports for the CLI.
4
+ *
5
+ * The split exists to keep the worker bundle clean: `defineDomain` (in the
6
+ * author's `domain.ts`) carries only worker-safe modules and is what the
7
+ * generated worker imports; the adapter (`cloudflare(...)` / `astrale(...)`) is
8
+ * node-only code that the worker must never pull in. `deploy(domain, adapter)`
9
+ * is authored in `astrale.config.ts` — a Node-only module the CLI loads but the
10
+ * worker never imports — so the adapter stays out of the bundle.
11
+ *
12
+ * export const domain = defineDomain({ schema, methods, deps, views, client })
13
+ *
14
+ * import { domain } from './domain'
15
+ * export default deploy(domain, cloudflare({ dev, prod }))
16
+ *
17
+ * The returned shape is just the definition with `adapter` attached — exactly
18
+ * what the CLI already reads (`def.adapter`, `def.origin`, `def.schema`, …), so
19
+ * the loader is unchanged.
20
+ */
21
+
22
+ import type { DomainAdapter } from './adapter'
23
+ import type { DomainDefinition } from './define-domain'
24
+
25
+ /** A domain definition bound to its deployment adapter — the CLI's input. */
26
+ export interface DeployConfig<Params = unknown> extends DomainDefinition {
27
+ adapter: DomainAdapter<Params>
28
+ }
29
+
30
+ export function deploy<Params>(
31
+ domain: DomainDefinition,
32
+ adapter: DomainAdapter<Params>,
33
+ ): DeployConfig<Params> {
34
+ return { ...domain, adapter }
35
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * `@astrale-os/sdk` config surface — the author-facing declaration of a
3
+ * standalone domain (`astrale.config.ts`) and the deployment-adapter contract.
4
+ *
5
+ * import { defineDomain, deploy } from '@astrale-os/sdk'
6
+ * import { cloudflare } from '@astrale-os/adapter-cloudflare'
7
+ *
8
+ * `defineDomain` (in `domain.ts`) declares the WORKER-SAFE domain — what it is +
9
+ * its identity, no adapter. `deploy(domain, adapter)` (in `astrale.config.ts`)
10
+ * binds it to a deployment target; the `astrale-domain` bin (folded into this
11
+ * package — see `../cli`) reads that default export and drives the adapter.
12
+ * Neither builds a server nor boots a kernel — they validate and package the
13
+ * declaration.
14
+ */
15
+
16
+ export { defineDomain } from './define-domain'
17
+ export type { ClientBinding, DefineDomainConfig, DomainDefinition } from './define-domain'
18
+
19
+ export { deploy } from './deploy'
20
+ export type { DeployConfig } from './deploy'
21
+
22
+ export { defineAdapter } from './adapter'
23
+ export type {
24
+ AdapterSpec,
25
+ DeployCtx,
26
+ DeployResult,
27
+ DomainAdapter,
28
+ DomainInfo,
29
+ WatchCtx,
30
+ WatchHandle,
31
+ } from './adapter'
@@ -24,8 +24,13 @@ import type { Context } from 'hono'
24
24
  import type { z } from 'zod'
25
25
 
26
26
  import type { CallRemoteFn } from '../dispatch/call-remote'
27
+ import type { KernelForAuth } from '../method/context'
27
28
 
28
- export type RemoteFunctionContext<TParams, TDeps = unknown> = {
29
+ export type RemoteFunctionContext<
30
+ TParams,
31
+ TDeps = unknown,
32
+ TKernel = BoundClientSessionView<FnMap> | null,
33
+ > = {
29
34
  /** Validated params (Zod-checked against `inputSchema`). */
30
35
  params: TParams
31
36
  /** Hono request context — escape hatch for headers, raw body, etc. */
@@ -37,10 +42,12 @@ export type RemoteFunctionContext<TParams, TDeps = unknown> = {
37
42
  /**
38
43
  * `BoundClientSessionView` to the parent kernel, bound to the composed
39
44
  * credential `union(delegation, self)` — same shape as `RemoteContext.kernel`
40
- * for `remoteMethod`. `null` when `auth: 'public'`, or `auth: 'optional'`
41
- * with no inbound credential.
45
+ * for `remoteMethod`. Nullability follows {@link KernelForAuth} of the
46
+ * function's `auth`: non-null for the default `'required'`, `… | null` for
47
+ * `'optional'`, `null` for `'public'` (use {@link RemoteFunctionContext.selfKernel}
48
+ * there, after verifying the upstream).
42
49
  */
43
- kernel: BoundClientSessionView<FnMap> | null
50
+ kernel: TKernel
44
51
  /**
45
52
  * Call another worker's remote method, re-minting the credential for the
46
53
  * target's audience. Same contract as {@link RemoteContext.callRemote}: throws
@@ -60,7 +67,12 @@ export type RemoteFunctionContext<TParams, TDeps = unknown> = {
60
67
  selfKernel: (kernelUrl?: string) => Promise<BoundClientSessionView<FnMap>>
61
68
  }
62
69
 
63
- export type RemoteFunctionDef<TParams = unknown, TResult = unknown, TDeps = unknown> = {
70
+ export type RemoteFunctionDef<
71
+ TParams = unknown,
72
+ TResult = unknown,
73
+ TDeps = unknown,
74
+ TAuth extends AuthPolicy = 'required',
75
+ > = {
64
76
  /**
65
77
  * Canonical callable identity. Auto-derived as `function.<slug>` (where
66
78
  * `<slug>` is the map key) when omitted. Stored as `Function.ref` on the
@@ -82,25 +94,42 @@ export type RemoteFunctionDef<TParams = unknown, TResult = unknown, TDeps = unkn
82
94
  * placeholders both supported.
83
95
  */
84
96
  binding?: FunctionBinding
85
- /** Authentication policy. Defaults to `'required'`. */
86
- auth?: AuthPolicy
97
+ /**
98
+ * Authentication policy. Defaults to `'required'`. Captured as a literal type
99
+ * so it drives {@link KernelForAuth} on the `execute`/`authorize` context:
100
+ * omit it (or `'required'`) → `ctx.kernel` non-null; `'optional'` → `… | null`;
101
+ * `'public'` → `null` (webhooks reach the graph via `ctx.selfKernel`).
102
+ */
103
+ auth?: TAuth
87
104
  /** Optional pre-execute authorization. Throw to deny. */
88
- authorize?: (ctx: RemoteFunctionContext<TParams, TDeps>) => void | Promise<void>
105
+ authorize?: (
106
+ ctx: RemoteFunctionContext<TParams, TDeps, KernelForAuth<TAuth>>,
107
+ ) => void | Promise<void>
89
108
  /** The function body. May be async. */
90
- execute: (ctx: RemoteFunctionContext<TParams, TDeps>) => TResult | Promise<TResult>
109
+ execute: (
110
+ ctx: RemoteFunctionContext<TParams, TDeps, KernelForAuth<TAuth>>,
111
+ ) => TResult | Promise<TResult>
91
112
  /** Optional human-readable description. */
92
113
  description?: string
93
114
  }
94
115
 
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.
95
119
  // oxlint-disable-next-line no-explicit-any
96
- export type AnyRemoteFunctionDef = RemoteFunctionDef<any, any, any>
120
+ export type AnyRemoteFunctionDef = RemoteFunctionDef<any, any, any, AuthPolicy>
97
121
 
98
122
  /**
99
123
  * Identity helper for authoring a RemoteFunction. Returns its argument
100
124
  * unchanged — `defineRemoteDomain` consumes the typed shape.
101
125
  */
102
- export function defineRemoteFunction<TParams, TResult, TDeps = unknown>(
103
- def: RemoteFunctionDef<TParams, TResult, TDeps>,
104
- ): RemoteFunctionDef<TParams, TResult, TDeps> {
126
+ export function defineRemoteFunction<
127
+ TParams,
128
+ TResult,
129
+ TDeps = unknown,
130
+ TAuth extends AuthPolicy = 'required',
131
+ >(
132
+ def: RemoteFunctionDef<TParams, TResult, TDeps, TAuth>,
133
+ ): RemoteFunctionDef<TParams, TResult, TDeps, TAuth> {
105
134
  return def
106
135
  }
@@ -22,8 +22,13 @@
22
22
 
23
23
  import type { ClientSessionCallOptions } from '@astrale-os/kernel-client/session'
24
24
 
25
- /** Minimal kernel-call surface callRemote needs (the bound view satisfies it). */
26
- type KernelCaller = {
25
+ /**
26
+ * The minimal kernel-call surface: dynamic method dispatch by path string. The
27
+ * bound kernel session view (`BoundClientSessionView`) satisfies it structurally,
28
+ * as does any test double. Exported as the public narrow caller contract so
29
+ * handlers can depend on it instead of the wide, generic session view.
30
+ */
31
+ export type KernelCaller = {
27
32
  call: (method: string, params: unknown, opts?: ClientSessionCallOptions) => Promise<unknown>
28
33
  }
29
34
 
@@ -25,7 +25,7 @@ import { MethodNotFoundError, SdkResultValidationError, SdkValidationError } fro
25
25
  import { executeHandler } from './execute'
26
26
  import { buildIdentityMap } from './identity'
27
27
  import { resolveMethod } from './resolve'
28
- import { resolveSelf, type SelfResult } from './self'
28
+ import { resolveSelf, withNode, type ParsedSelf } from './self'
29
29
  import { validateParams, validateResult } from './validate'
30
30
 
31
31
  export type SdkDispatcherConfig<TDeps> = {
@@ -139,7 +139,7 @@ export class SdkDispatcher<TDeps = unknown> implements KernelAPI {
139
139
 
140
140
  // `undefined` (not `null`) for static methods — matches the `self` type on
141
141
  // `RemoteContext` / `MethodImpl`, which is `undefined` for static methods.
142
- let resolvedSelf: SelfResult | undefined
142
+ let parsedSelf: ParsedSelf | undefined
143
143
  if (!bound.isStatic) {
144
144
  if (selfRef === undefined || selfRef === null || selfRef === '') {
145
145
  throw new SdkValidationError(
@@ -151,7 +151,7 @@ export class SdkDispatcher<TDeps = unknown> implements KernelAPI {
151
151
  )
152
152
  }
153
153
  try {
154
- resolvedSelf = resolveSelf(String(selfRef))
154
+ parsedSelf = resolveSelf(String(selfRef))
155
155
  } catch (err) {
156
156
  // A malformed caller-supplied `self` is bad client input, not a server
157
157
  // fault — surface it as VALIDATION_ERROR (422), matching the sibling
@@ -174,10 +174,14 @@ export class SdkDispatcher<TDeps = unknown> implements KernelAPI {
174
174
  }
175
175
  const handlerParams = validation.data
176
176
  const callRemote = makeCallRemote(kernel)
177
+ // Enrich the parsed self with the lazy `node()` accessor, bound to this
178
+ // request's kernel (null when the auth policy yields none — `node()` then
179
+ // rejects). Done here, not in `resolveSelf`, so the parse stays kernel-free.
180
+ const self = parsedSelf ? withNode(parsedSelf, kernel) : undefined
177
181
  const ctx = {
178
182
  params: handlerParams,
179
183
  auth,
180
- self: resolvedSelf,
184
+ self,
181
185
  deps: this.deps,
182
186
  url: this.url,
183
187
  kernel,
@@ -8,7 +8,7 @@ export {
8
8
  } from './validate'
9
9
  export { resolveSelf, type SelfResult } from './self'
10
10
  export { executeHandler, type ExecuteParams } from './execute'
11
- export { makeCallRemote, type CallRemoteFn } from './call-remote'
11
+ export { makeCallRemote, type CallRemoteFn, type KernelCaller } from './call-remote'
12
12
  export {
13
13
  AuthorizationDeniedError,
14
14
  MethodNotFoundError,