@astrale-os/sdk 0.1.6 → 0.1.8

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 (103) hide show
  1. package/dist/cli/bin.d.ts +7 -0
  2. package/dist/cli/bin.d.ts.map +1 -0
  3. package/dist/cli/bin.js +15 -0
  4. package/dist/cli/bin.js.map +1 -0
  5. package/dist/cli/dotenv.d.ts +13 -0
  6. package/dist/cli/dotenv.d.ts.map +1 -0
  7. package/dist/cli/dotenv.js +46 -0
  8. package/dist/cli/dotenv.js.map +1 -0
  9. package/dist/cli/index.d.ts +15 -0
  10. package/dist/cli/index.d.ts.map +1 -0
  11. package/dist/cli/index.js +15 -0
  12. package/dist/cli/index.js.map +1 -0
  13. package/dist/cli/run.d.ts +84 -0
  14. package/dist/cli/run.d.ts.map +1 -0
  15. package/dist/cli/run.js +603 -0
  16. package/dist/cli/run.js.map +1 -0
  17. package/dist/cli/spec.d.ts +19 -0
  18. package/dist/cli/spec.d.ts.map +1 -0
  19. package/dist/cli/spec.js +31 -0
  20. package/dist/cli/spec.js.map +1 -0
  21. package/dist/config/adapter.d.ts +140 -0
  22. package/dist/config/adapter.d.ts.map +1 -0
  23. package/dist/config/adapter.js +40 -0
  24. package/dist/config/adapter.js.map +1 -0
  25. package/dist/config/define-domain.d.ts +112 -0
  26. package/dist/config/define-domain.d.ts.map +1 -0
  27. package/dist/config/define-domain.js +98 -0
  28. package/dist/config/define-domain.js.map +1 -0
  29. package/dist/config/deploy.d.ts +28 -0
  30. package/dist/config/deploy.d.ts.map +1 -0
  31. package/dist/config/deploy.js +24 -0
  32. package/dist/config/deploy.js.map +1 -0
  33. package/dist/config/index.d.ts +21 -0
  34. package/dist/config/index.d.ts.map +1 -0
  35. package/dist/config/index.js +18 -0
  36. package/dist/config/index.js.map +1 -0
  37. package/dist/define/remote-function.d.ts +19 -11
  38. package/dist/define/remote-function.d.ts.map +1 -1
  39. package/dist/define/remote-function.js.map +1 -1
  40. package/dist/dispatch/call-remote.d.ts +7 -3
  41. package/dist/dispatch/call-remote.d.ts.map +1 -1
  42. package/dist/dispatch/call-remote.js.map +1 -1
  43. package/dist/dispatch/dispatcher.d.ts.map +1 -1
  44. package/dist/dispatch/dispatcher.js +8 -4
  45. package/dist/dispatch/dispatcher.js.map +1 -1
  46. package/dist/dispatch/index.d.ts +1 -1
  47. package/dist/dispatch/index.d.ts.map +1 -1
  48. package/dist/dispatch/index.js.map +1 -1
  49. package/dist/dispatch/self.d.ts +46 -10
  50. package/dist/dispatch/self.d.ts.map +1 -1
  51. package/dist/dispatch/self.js +65 -8
  52. package/dist/dispatch/self.js.map +1 -1
  53. package/dist/domain/define.d.ts +3 -3
  54. package/dist/domain/define.js +3 -3
  55. package/dist/index.d.ts +5 -4
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +8 -2
  58. package/dist/index.js.map +1 -1
  59. package/dist/method/class.d.ts.map +1 -1
  60. package/dist/method/class.js.map +1 -1
  61. package/dist/method/context.d.ts +32 -7
  62. package/dist/method/context.d.ts.map +1 -1
  63. package/dist/method/index.d.ts +1 -1
  64. package/dist/method/index.d.ts.map +1 -1
  65. package/dist/method/single.d.ts +16 -11
  66. package/dist/method/single.d.ts.map +1 -1
  67. package/dist/method/single.js.map +1 -1
  68. package/dist/server/domain-entry.d.ts +67 -0
  69. package/dist/server/domain-entry.d.ts.map +1 -0
  70. package/dist/server/domain-entry.js +58 -0
  71. package/dist/server/domain-entry.js.map +1 -0
  72. package/dist/server/index.d.ts +3 -1
  73. package/dist/server/index.d.ts.map +1 -1
  74. package/dist/server/index.js +2 -1
  75. package/dist/server/index.js.map +1 -1
  76. package/dist/server/worker-entry.d.ts +80 -5
  77. package/dist/server/worker-entry.d.ts.map +1 -1
  78. package/dist/server/worker-entry.js +105 -24
  79. package/dist/server/worker-entry.js.map +1 -1
  80. package/package.json +12 -3
  81. package/src/cli/bin.ts +15 -0
  82. package/src/cli/dotenv.ts +45 -0
  83. package/src/cli/index.ts +15 -0
  84. package/src/cli/run.ts +710 -0
  85. package/src/cli/spec.ts +42 -0
  86. package/src/config/adapter.ts +172 -0
  87. package/src/config/define-domain.ts +218 -0
  88. package/src/config/deploy.ts +35 -0
  89. package/src/config/index.ts +31 -0
  90. package/src/define/remote-function.ts +42 -13
  91. package/src/dispatch/call-remote.ts +7 -2
  92. package/src/dispatch/dispatcher.ts +8 -4
  93. package/src/dispatch/index.ts +1 -1
  94. package/src/dispatch/self.ts +96 -10
  95. package/src/domain/define.ts +3 -3
  96. package/src/index.ts +25 -4
  97. package/src/method/class.ts +4 -3
  98. package/src/method/context.ts +38 -7
  99. package/src/method/index.ts +1 -1
  100. package/src/method/single.ts +30 -11
  101. package/src/server/domain-entry.ts +113 -0
  102. package/src/server/index.ts +3 -1
  103. package/src/server/worker-entry.ts +128 -24
@@ -6,11 +6,13 @@
6
6
  * • resolve the serving URL (== `iss`), canonicalize it (so the value matches
7
7
  * what `createRemoteServer` signs with and the kernel pins), and cache the
8
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;
9
+ * • optional subrequest routing: a `globalThis.fetch` override (installed ONLY
10
+ * when `selfBinding` and/or `routeSubrequest` is configured) redirects
11
+ * certain outbound fetches through a caller-supplied binding `selfBinding`
12
+ * for same-host fetches (a Worker can't fetch its own hostname), and the
13
+ * vendor-neutral `routeSubrequest` for any caller policy (e.g. instance
14
+ * hostnames a same-zone Worker→Worker fetch would 522 on). The SDK names no
15
+ * backend or topology; the caller owns the predicate + the fetcher;
14
16
  * • optional SPA hook (e.g. `/ui/*` served from an `ASSETS` binding).
15
17
  *
16
18
  * The worker's OWN JWKS (`<url>/.well-known/jwks.json`) is served as a normal
@@ -29,6 +31,54 @@ import { canonicalizeServingUrl } from './serving-url'
29
31
  type Fetcher = { fetch(request: Request): Response | Promise<Response> }
30
32
  type App = { fetch(request: Request): Response | Promise<Response> }
31
33
 
34
+ /** A built app cached by its serving URL: `origin` for same-origin matching,
35
+ * `app` for in-process self-dispatch. */
36
+ type CachedApp = { origin: string; app: App }
37
+
38
+ /**
39
+ * Choose where an OUTBOUND subrequest to `u` is routed — or `null` to let the
40
+ * real `fetch` handle it. Order matters and same-origin wins FIRST: a call to
41
+ * this worker's OWN serving origin is a self-dispatch, not an edge fetch — and
42
+ * the caller's `routeSubrequest` policy may itself match our own host (e.g. an
43
+ * `isInstanceHost` predicate matches every `*.svc.astrale.ai`, ours included),
44
+ * so it must never get first look at a same-origin call.
45
+ *
46
+ * • same-origin + SELF binding present → the SELF fetcher: a fresh same-script
47
+ * invocation. The normal Cloudflare path — a Worker can't fetch its own
48
+ * hostname over the edge, so it re-enters itself through the binding.
49
+ * • same-origin + NO SELF binding → the cached app, dispatched IN-PROCESS. A
50
+ * Workers-for-Platforms dispatch-namespace tenant can't service-bind to
51
+ * itself (its script name is platform-renamed), so it has no SELF binding; an
52
+ * in-process dispatch reaches the same script with no edge hop and no binding.
53
+ * `App` and the SELF `Fetcher` share the `{ fetch(request) }` shape, so the
54
+ * caller drives both identically.
55
+ * • else, the caller's `routeSubrequest` policy matched → its fetcher.
56
+ * • else → `null`: passthrough to the real network fetch.
57
+ *
58
+ * Pure (no closure over instance state) so the routing decision is unit-testable
59
+ * without standing up a real app.
60
+ */
61
+ export function selectSubrequestTarget<TDeps>(
62
+ u: URL,
63
+ ctx: {
64
+ self: Fetcher | null
65
+ apps: Iterable<CachedApp>
66
+ routeEnv: TDeps | null
67
+ routeSubrequest?: (url: URL, env: TDeps) => Fetcher | null | undefined
68
+ },
69
+ ): { fetch(request: Request): Response | Promise<Response> } | null {
70
+ // Same-origin self-subrequest → the same script (SELF binding, else in-process).
71
+ for (const cached of ctx.apps) {
72
+ if (cached.origin === u.origin) return ctx.self ?? cached.app
73
+ }
74
+ // Caller policy (e.g. a platform router on the same zone) → its fetcher.
75
+ if (ctx.routeSubrequest && ctx.routeEnv) {
76
+ const via = ctx.routeSubrequest(u, ctx.routeEnv)
77
+ if (via) return via
78
+ }
79
+ return null
80
+ }
81
+
32
82
  export interface WorkerEntryConfig<TDeps> {
33
83
  /**
34
84
  * Build the `createRemoteServer` config for the resolved serving `url`. Called
@@ -49,6 +99,17 @@ export interface WorkerEntryConfig<TDeps> {
49
99
  resolveUrl?: (env: TDeps, requestOrigin: string) => string
50
100
  /** Optional: the `SELF` service binding used to route same-host subrequests. */
51
101
  selfBinding?: (env: TDeps) => Fetcher | null | undefined
102
+ /**
103
+ * Optional, vendor-neutral: route an OUTBOUND subrequest through a
104
+ * caller-supplied fetcher instead of the network — for hosts a Worker can't
105
+ * reach directly over the edge (e.g. a platform router on the SAME zone:
106
+ * Cloudflare 522s a same-zone Worker→Worker public `fetch`). Return a `Fetcher`
107
+ * to route the request through, or null/undefined to fall through to the normal
108
+ * fetch. The SDK names no backend or topology — the CALLER owns BOTH the
109
+ * predicate (which hosts) and the fetcher (the binding). This generalizes
110
+ * `selfBinding` (same-origin → SELF) to any caller policy; both are honored.
111
+ */
112
+ routeSubrequest?: (url: URL, env: TDeps) => Fetcher | null | undefined
52
113
  /**
53
114
  * Optional: handle a request before it reaches the kernel app — e.g. serve a
54
115
  * SPA under `/ui/*` or a same-origin `/api/*` endpoint the view calls. Return
@@ -91,6 +152,46 @@ export function clientOrigin(url: URL, request: Request): string {
91
152
  return scheme === 'https' ? `https://${url.host}` : url.origin
92
153
  }
93
154
 
155
+ /**
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.
160
+ *
161
+ * Asset URLs are rooted at `base` (a client bundler sets `base: '<base>/'`);
162
+ * this hook strips that prefix before delegating to the binding, so
163
+ * `<base>/x.js` resolves from the binding's root. When `devProxy` yields a URL
164
+ * (local dev), requests are proxied there instead — the seam for a bundler's
165
+ * HMR dev server. Whether unknown sub-paths fall back to `index.html` is the
166
+ * binding's own concern (e.g. wrangler's `not_found_handling`), not baked in
167
+ * here.
168
+ *
169
+ * Lives here, type-checked and testable, instead of being emitted as a string
170
+ * by every adapter's worker codegen.
171
+ */
172
+ export function assets<TDeps>(opts: {
173
+ base?: string
174
+ binding: (env: TDeps) => Fetcher | null | undefined
175
+ devProxy?: (env: TDeps) => string | null | undefined
176
+ }): (env: TDeps, url: URL, request: Request) => Response | Promise<Response> | undefined {
177
+ const base = (opts.base ?? '/ui').replace(/\/+$/, '')
178
+ const prefix = `${base}/`
179
+ return (env, url, request) => {
180
+ const binding = opts.binding(env)
181
+ if (!binding) return undefined
182
+ if (url.pathname !== base && !url.pathname.startsWith(prefix)) return undefined
183
+ const devUrl = opts.devProxy?.(env)
184
+ if (devUrl) {
185
+ const devBase = devUrl.replace(/\/+$/, '')
186
+ return fetch(new Request(`${devBase}${url.pathname}${url.search}`, request))
187
+ }
188
+ // `<base>` → `/`, `<base>/x` → `/x`: serve from the binding's root.
189
+ const stripped = url.pathname.slice(base.length) || '/'
190
+ const rewritten = new URL(stripped + url.search, url.origin)
191
+ return binding.fetch(new Request(rewritten, request))
192
+ }
193
+ }
194
+
94
195
  export function createWorkerEntry<TDeps>(config: WorkerEntryConfig<TDeps>): WorkerEntry<TDeps> {
95
196
  // Cache the built app per distinct resolved URL — plural and bounded. On the
96
197
  // request-origin fallback the URL legitimately alternates for one worker
@@ -99,8 +200,9 @@ export function createWorkerEntry<TDeps>(config: WorkerEntryConfig<TDeps>): Work
99
200
  // every alternation. The bound caps abuse via attacker-minted Host /
100
201
  // X-Forwarded-Proto values on that same fallback path.
101
202
  const MAX_CACHED_APPS = 4
102
- const apps = new Map<string, { origin: string; app: App }>()
203
+ const apps = new Map<string, CachedApp>()
103
204
  let self: Fetcher | null = null
205
+ let routeEnv: TDeps | null = null
104
206
 
105
207
  function getApp(url: string, env: TDeps): App {
106
208
  const cached = apps.get(url)
@@ -114,26 +216,27 @@ export function createWorkerEntry<TDeps>(config: WorkerEntryConfig<TDeps>): Work
114
216
  return app
115
217
  }
116
218
 
117
- // A Worker can't fetch its own hostname. When a SELF service binding is
118
- // configured, route same-host subrequests (e.g. `ctx.callRemote` back into
119
- // this domain) through it. This is the only reason to override
120
- // `globalThis.fetch` workers without a `selfBinding` get no global mutation.
121
- // (A self-issued credential's JWKS is resolved in-memory by the verifier, not
122
- // fetched see `auth/verify.ts` so no self-JWKS interception is needed.)
123
- if (config.selfBinding) {
219
+ // A Worker can't fetch its own hostname over the edge, nor on Cloudflare — a
220
+ // same-zone hostname routed to another Worker (the `routeSubrequest` case).
221
+ // When either is configured, override `globalThis.fetch` to redirect those
222
+ // subrequests through the right target (see `selectSubrequestTarget`). Workers
223
+ // with neither selfBinding nor routeSubrequest get no global mutation. (A
224
+ // self-issued credential's JWKS is resolved in-memory by the verifier, not
225
+ // fetched — see `auth/verify.ts` — so no self-JWKS shim is needed.)
226
+ if (config.selfBinding || config.routeSubrequest) {
124
227
  const originalFetch = globalThis.fetch
125
228
  globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
126
- if (apps.size > 0 && self) {
127
- const href =
128
- typeof input === 'string' ? input : input instanceof URL ? input.href : input.url
129
- try {
130
- const origin = new URL(href).origin
131
- for (const cached of apps.values()) {
132
- if (cached.origin === origin) return self.fetch(new Request(input, init))
133
- }
134
- } catch {
135
- // non-absolute URL — fall through to the original fetch
136
- }
229
+ const href = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url
230
+ try {
231
+ const target = selectSubrequestTarget(new URL(href), {
232
+ self,
233
+ apps: apps.values(),
234
+ routeEnv,
235
+ routeSubrequest: config.routeSubrequest,
236
+ })
237
+ if (target) return target.fetch(new Request(input, init))
238
+ } catch {
239
+ // non-absolute URL — fall through to the original fetch
137
240
  }
138
241
  return originalFetch(input, init)
139
242
  }) as typeof fetch
@@ -142,6 +245,7 @@ export function createWorkerEntry<TDeps>(config: WorkerEntryConfig<TDeps>): Work
142
245
  return {
143
246
  async fetch(request: Request, env: TDeps): Promise<Response> {
144
247
  if (config.selfBinding) self ??= config.selfBinding(env) ?? null
248
+ if (config.routeSubrequest) routeEnv = env
145
249
  // Only parse the request URL when a hook actually needs it.
146
250
  const requestUrl = config.before || config.resolveUrl ? new URL(request.url) : null
147
251
  if (config.before && requestUrl) {