@astrale-os/sdk 0.1.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 (50) hide show
  1. package/README.md +42 -0
  2. package/package.json +101 -0
  3. package/src/auth/authenticate.ts +51 -0
  4. package/src/auth/check.ts +73 -0
  5. package/src/auth/compose.ts +31 -0
  6. package/src/auth/errors.ts +32 -0
  7. package/src/auth/identity.ts +15 -0
  8. package/src/auth/index.ts +11 -0
  9. package/src/auth/kernel-client.ts +107 -0
  10. package/src/auth/resolve.ts +63 -0
  11. package/src/auth/sign.ts +36 -0
  12. package/src/auth/verify.ts +138 -0
  13. package/src/define/index.ts +9 -0
  14. package/src/define/remote-function.ts +96 -0
  15. package/src/define/view.ts +91 -0
  16. package/src/deploy/check.ts +124 -0
  17. package/src/deploy/hash-spec.ts +31 -0
  18. package/src/deploy/index.ts +3 -0
  19. package/src/deploy/meta.ts +25 -0
  20. package/src/dispatch/authorize.ts +29 -0
  21. package/src/dispatch/call-remote.ts +48 -0
  22. package/src/dispatch/dispatcher.ts +257 -0
  23. package/src/dispatch/errors.ts +94 -0
  24. package/src/dispatch/execute.ts +51 -0
  25. package/src/dispatch/identity.ts +148 -0
  26. package/src/dispatch/index.ts +17 -0
  27. package/src/dispatch/resolve.ts +78 -0
  28. package/src/dispatch/self.ts +32 -0
  29. package/src/dispatch/validate.ts +41 -0
  30. package/src/domain/build-spec.ts +127 -0
  31. package/src/domain/contract.ts +41 -0
  32. package/src/domain/define.ts +168 -0
  33. package/src/domain/extend-core.ts +287 -0
  34. package/src/domain/index.ts +4 -0
  35. package/src/index.ts +77 -0
  36. package/src/method/class.ts +148 -0
  37. package/src/method/context.ts +45 -0
  38. package/src/method/index.ts +5 -0
  39. package/src/method/single.ts +133 -0
  40. package/src/server/auxiliary-routes.ts +336 -0
  41. package/src/server/config.ts +85 -0
  42. package/src/server/create.ts +249 -0
  43. package/src/server/handle.ts +37 -0
  44. package/src/server/index.ts +10 -0
  45. package/src/server/jwks.ts +18 -0
  46. package/src/server/require-env.ts +19 -0
  47. package/src/server/serving-url.ts +28 -0
  48. package/src/server/start.ts +37 -0
  49. package/src/server/worker-entry.ts +122 -0
  50. package/src/server/worker-meta.ts +25 -0
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `RemoteServer` and `RemoteServerHandle` — output shapes for the SDK server.
3
+ *
4
+ * `RemoteServer` is what `createRemoteServer` returns: the assembled Hono
5
+ * app plus a Node convenience helper. `RemoteServerHandle` is what `start()`
6
+ * resolves to: the bound port and a `close()` function for graceful shutdown.
7
+ */
8
+
9
+ import type { Hono } from 'hono'
10
+
11
+ export type RemoteServer = {
12
+ /**
13
+ * The assembled Hono app. Use `app.fetch` for any runtime
14
+ * (Bun, Deno, Cloudflare Workers, or Node via `@hono/node-server`).
15
+ */
16
+ app: Hono
17
+ /**
18
+ * The worker's canonical serving URL — its `iss` identity (full URL, trailing
19
+ * slash stripped, base path preserved). The single value the dispatcher signs
20
+ * with and the kernel pins. Read it to seed `Identity.iss` on graph nodes so
21
+ * the seeded value matches the signing issuer (never re-derive from a raw env).
22
+ */
23
+ iss: string
24
+ /**
25
+ * Convenience helper: start a Node HTTP server on the given port using
26
+ * `@hono/node-server` (loaded via dynamic import). For other runtimes,
27
+ * use `app.fetch` directly.
28
+ */
29
+ start: (port?: number) => Promise<RemoteServerHandle>
30
+ }
31
+
32
+ export type RemoteServerHandle = {
33
+ /** The port the server is listening on (resolved even if `port: 0` was requested). */
34
+ port: number
35
+ /** Stop the server and release the port. */
36
+ close: () => Promise<void>
37
+ }
@@ -0,0 +1,10 @@
1
+ export { createRemoteServer } from './create'
2
+ export type { RemoteServerConfig } from './config'
3
+ export type { RemoteServer, RemoteServerHandle } from './handle'
4
+ export { derivePublicJwk } from './jwks'
5
+ export { requireEnv } from './require-env'
6
+ export { canonicalizeServingUrl } from './serving-url'
7
+ export { createWorkerEntry } from './worker-entry'
8
+ export type { WorkerEntry, WorkerEntryConfig } from './worker-entry'
9
+ export { workerMeta } from './worker-meta'
10
+ export { startNodeServer } from './start'
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Derive a public JWK from a private one.
3
+ *
4
+ * Strips the private components for both EC keys (drops `d`) and RSA keys
5
+ * (drops `d`, `p`, `q`, `dp`, `dq`, `qi`, and `oth` — the additional-primes
6
+ * array on multi-prime RSA keys). The result is safe to publish at
7
+ * `<url>/.well-known/jwks.json` so downstream verifiers can validate
8
+ * signatures the server produced.
9
+ */
10
+
11
+ export function derivePublicJwk(privateKey: JsonWebKey): Record<string, unknown> {
12
+ // oxlint-disable-next-line no-unused-vars
13
+ const { d, p, q, dp, dq, qi, oth, ...publicJwk } = privateKey as unknown as Record<
14
+ string,
15
+ unknown
16
+ >
17
+ return publicJwk
18
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Read a string-typed env var (process.env on Node, the Worker `env` binding
3
+ * on Cloudflare) and throw a clear error if it is missing or empty.
4
+ *
5
+ * Use this for **identity-bearing** values (issuer, baseDomain, audience,
6
+ * worker URL) where a guessed default silently mints wrong JWTs or bakes
7
+ * wrong refs into persisted artifacts. The conventional `?? 'literal'`
8
+ * fallback is the anti-pattern this exists to replace.
9
+ *
10
+ * In local dev these live in each domain worker's `worker/.dev.vars` (read
11
+ * natively by `wrangler dev`), so this throw never fires in a properly-prepared
12
+ * dev environment — only when wiring is genuinely missing.
13
+ */
14
+ export function requireEnv(env: unknown, name: string, hint?: string): string {
15
+ const v = (env as Record<string, unknown> | null | undefined)?.[name]
16
+ if (typeof v === 'string' && v.length > 0) return v
17
+ const help = hint ? ` (${hint})` : ''
18
+ throw new Error(`Missing required env var: ${name}${help}`)
19
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Canonicalize a worker's serving URL into its `iss` identity.
3
+ *
4
+ * Returns the full URL with trailing slash(es) stripped — the base path is
5
+ * preserved (an issuer may carry a non-empty path, like `…/kernel/host`). This
6
+ * is the single canonical form used for outbound signing, the JWKS issuer,
7
+ * `/meta`, the install credential, and any `Identity.iss` a domain seeds, so the
8
+ * value a worker SIGNS always matches the value the kernel LOOKS UP (the kernel
9
+ * canonicalizes the verified `iss` the same way and does an exact-match lookup).
10
+ *
11
+ * Throws if `url` is not a parseable absolute URL.
12
+ */
13
+ // INVARIANT (cross-repo): issuer PRODUCERS must strip trailing path slashes
14
+ // BEFORE a value becomes an iss. kernel-core's `normalizeIssuerId` only strips
15
+ // a lone trailing slash on an EMPTY path, so `https://x/base/` and
16
+ // `https://x/base` are distinct issuer identities to the kernel — this
17
+ // function and the kernel's `installSourceBase` both pre-strip with the same
18
+ // rule so the two sides can never mint diverging identities for one worker.
19
+ export function canonicalizeServingUrl(url: string): string {
20
+ try {
21
+ return new URL(url).href.replace(/\/+$/, '')
22
+ } catch {
23
+ throw new Error(
24
+ `canonicalizeServingUrl: expected a full URL (got "${url}") — ` +
25
+ 'it is the worker serving URL and its JWT issuer identity.',
26
+ )
27
+ }
28
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Node HTTP convenience starter.
3
+ *
4
+ * Dynamically imports `@hono/node-server` so the SDK stays runtime-agnostic
5
+ * — only Node consumers actually pull in the dep. For Bun / Deno /
6
+ * Cloudflare, consumers go through `server.app.fetch` directly.
7
+ */
8
+
9
+ import type { Hono } from 'hono'
10
+
11
+ import type { RemoteServerHandle } from './handle'
12
+
13
+ export async function startNodeServer(app: Hono, port = 3000): Promise<RemoteServerHandle> {
14
+ const { serve } = await import('@hono/node-server')
15
+ // oxlint-disable-next-line no-explicit-any
16
+ const server = serve({ fetch: app.fetch, port }) as any
17
+
18
+ await new Promise<void>((resolve, reject) => {
19
+ if (server.listening) return resolve()
20
+ server.once('listening', () => resolve())
21
+ // Without this, a bind failure (e.g. EADDRINUSE on a busy port) emits
22
+ // `'error'` and never `'listening'`, leaving the Promise — and startup —
23
+ // hung forever with no diagnostic.
24
+ server.once('error', (err: Error) => reject(err))
25
+ })
26
+
27
+ const address = server.address() as { port: number; address: string } | null
28
+ const actualPort = address?.port ?? port
29
+
30
+ return {
31
+ port: actualPort,
32
+ close: () =>
33
+ new Promise<void>((resolve, reject) => {
34
+ server.close((err: Error | null) => (err ? reject(err) : resolve()))
35
+ }),
36
+ }
37
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * `createWorkerEntry` — the shared worker `fetch` plumbing every Astrale domain
3
+ * worker (and the cloudflare adapter's codegen) needs, so it lives in ONE place
4
+ * instead of being copy-pasted per worker:
5
+ *
6
+ * • resolve the serving URL (== `iss`), canonicalize it (so the value matches
7
+ * what `createRemoteServer` signs with and the kernel pins), and cache the
8
+ * built app per distinct URL;
9
+ * • optional self-binding routing: when a `SELF` service binding is provided,
10
+ * route same-host subrequests (e.g. `ctx.callRemote` back into this domain)
11
+ * through it — Cloudflare forbids a Worker fetching its own hostname. This is
12
+ * the ONLY reason a `globalThis.fetch` override is installed, and only when a
13
+ * `selfBinding` is configured;
14
+ * • optional SPA hook (e.g. `/ui/*` served from an `ASSETS` binding).
15
+ *
16
+ * The worker's OWN JWKS (`<url>/.well-known/jwks.json`) is served as a normal
17
+ * route by `createRemoteServer`; the verifier resolves a self-issued credential
18
+ * from the in-memory key (see `auth/verify.ts`), so no self-fetch shim is needed.
19
+ *
20
+ * The worker file is then just schema + methods + a `build(url, env)` callback.
21
+ */
22
+
23
+ import type { RemoteServerConfig } from './config'
24
+
25
+ import { createRemoteServer } from './create'
26
+ import { requireEnv } from './require-env'
27
+ import { canonicalizeServingUrl } from './serving-url'
28
+
29
+ type Fetcher = { fetch(request: Request): Response | Promise<Response> }
30
+ type App = { fetch(request: Request): Response | Promise<Response> }
31
+
32
+ export interface WorkerEntryConfig<TDeps> {
33
+ /**
34
+ * Build the `createRemoteServer` config for the resolved serving `url`. Called
35
+ * once per distinct URL (the resulting app is cached), with the same `env` the
36
+ * request carries — so it can read additional bindings (e.g. a base domain).
37
+ */
38
+ build: (url: string, env: TDeps) => RemoteServerConfig<TDeps>
39
+ /**
40
+ * Resolve the raw serving URL from `env` (+ the per-request origin, for workers
41
+ * that fall back to the request host). Defaults to the `WORKER_URL` env var.
42
+ * The result is always canonicalized before use.
43
+ */
44
+ resolveUrl?: (env: TDeps, requestOrigin: string) => string
45
+ /** Optional: the `SELF` service binding used to route same-host subrequests. */
46
+ selfBinding?: (env: TDeps) => Fetcher | null | undefined
47
+ /**
48
+ * Optional: handle a request before it reaches the kernel app — e.g. serve a
49
+ * SPA under `/ui/*` or a same-origin `/api/*` endpoint the view calls. Return
50
+ * a `Response` to short-circuit, or `undefined` to fall through to the domain
51
+ * dispatch.
52
+ */
53
+ before?: (
54
+ env: TDeps,
55
+ url: URL,
56
+ request: Request,
57
+ ) => Response | undefined | Promise<Response | undefined>
58
+ /**
59
+ * Optional: transform the request on the fall-through path, just before it
60
+ * reaches the kernel app (e.g. rewrite the hostname for wildcard-subdomain
61
+ * routing). Not applied when `before` short-circuits.
62
+ */
63
+ rewriteRequest?: (env: TDeps, request: Request) => Request
64
+ }
65
+
66
+ export interface WorkerEntry<TDeps> {
67
+ fetch(request: Request, env: TDeps): Response | Promise<Response>
68
+ }
69
+
70
+ export function createWorkerEntry<TDeps>(config: WorkerEntryConfig<TDeps>): WorkerEntry<TDeps> {
71
+ let cache: { url: string; origin: string; app: App } | null = null
72
+ let self: Fetcher | null = null
73
+
74
+ function getApp(url: string, env: TDeps): App {
75
+ if (cache && cache.url === url) return cache.app
76
+ const { app } = createRemoteServer<TDeps>(config.build(url, env))
77
+ cache = { url, origin: new URL(url).origin, app }
78
+ return app
79
+ }
80
+
81
+ // A Worker can't fetch its own hostname. When a SELF service binding is
82
+ // configured, route same-host subrequests (e.g. `ctx.callRemote` back into
83
+ // this domain) through it. This is the only reason to override
84
+ // `globalThis.fetch` — workers without a `selfBinding` get no global mutation.
85
+ // (A self-issued credential's JWKS is resolved in-memory by the verifier, not
86
+ // fetched — see `auth/verify.ts` — so no self-JWKS interception is needed.)
87
+ if (config.selfBinding) {
88
+ const originalFetch = globalThis.fetch
89
+ globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
90
+ if (cache && self) {
91
+ const href =
92
+ typeof input === 'string' ? input : input instanceof URL ? input.href : input.url
93
+ try {
94
+ if (new URL(href).origin === cache.origin) return self.fetch(new Request(input, init))
95
+ } catch {
96
+ // non-absolute URL — fall through to the original fetch
97
+ }
98
+ }
99
+ return originalFetch(input, init)
100
+ }) as typeof fetch
101
+ }
102
+
103
+ return {
104
+ async fetch(request: Request, env: TDeps): Promise<Response> {
105
+ if (config.selfBinding) self ??= config.selfBinding(env) ?? null
106
+ // Only parse the request URL when a hook actually needs it.
107
+ const requestUrl = config.before || config.resolveUrl ? new URL(request.url) : null
108
+ if (config.before && requestUrl) {
109
+ // Await so an async `before` that resolves to `undefined` falls through
110
+ // (a returned Promise<undefined> would otherwise be sent as the response).
111
+ const handled = await config.before(env, requestUrl, request)
112
+ if (handled !== undefined) return handled
113
+ }
114
+ const raw = config.resolveUrl
115
+ ? config.resolveUrl(env, requestUrl!.origin)
116
+ : requireEnv(env, 'WORKER_URL', "the worker's public serving URL (its iss identity)")
117
+ const url = canonicalizeServingUrl(raw)
118
+ const dispatched = config.rewriteRequest ? config.rewriteRequest(env, request) : request
119
+ return getApp(url, env).fetch(dispatched)
120
+ },
121
+ }
122
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Build the `/meta` provenance block for a domain worker: the build-injected
3
+ * `SDK_COMMIT` / `SCHEMA_HASH` globals plus the domain name. A field is omitted
4
+ * when its global isn't defined.
5
+ *
6
+ * `SDK_COMMIT` / `SCHEMA_HASH` are `--define`'d into the worker bundle at build
7
+ * time; the bundler applies that text replacement to this inlined helper too, so
8
+ * reading them here behaves exactly as the per-worker block this replaces.
9
+ */
10
+ declare const SDK_COMMIT: string | undefined
11
+ declare const SCHEMA_HASH: string | undefined
12
+
13
+ export function workerMeta(domainName: string): {
14
+ sdkCommit?: string
15
+ schemaHash?: string
16
+ domainName: string
17
+ } {
18
+ const sdkCommit = typeof SDK_COMMIT === 'string' ? SDK_COMMIT : undefined
19
+ const schemaHash = typeof SCHEMA_HASH === 'string' ? SCHEMA_HASH : undefined
20
+ return {
21
+ ...(sdkCommit ? { sdkCommit } : {}),
22
+ ...(schemaHash ? { schemaHash } : {}),
23
+ domainName,
24
+ }
25
+ }