@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
@@ -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
@@ -40,10 +42,26 @@ export interface WorkerEntryConfig<TDeps> {
40
42
  * Resolve the raw serving URL from `env` (+ the per-request origin, for workers
41
43
  * that fall back to the request host). Defaults to the `WORKER_URL` env var.
42
44
  * The result is always canonicalized before use.
45
+ *
46
+ * The `requestOrigin` honors an `X-Forwarded-Proto: https` upgrade (see
47
+ * `clientOrigin`), so a dev worker behind a TLS-terminating proxy (cloudflared
48
+ * tunnel, reverse proxy) resolves its public `https://` origin, not the raw
49
+ * `http://` one workerd sees.
43
50
  */
44
51
  resolveUrl?: (env: TDeps, requestOrigin: string) => string
45
52
  /** Optional: the `SELF` service binding used to route same-host subrequests. */
46
53
  selfBinding?: (env: TDeps) => Fetcher | null | undefined
54
+ /**
55
+ * Optional, vendor-neutral: route an OUTBOUND subrequest through a
56
+ * caller-supplied fetcher instead of the network — for hosts a Worker can't
57
+ * reach directly over the edge (e.g. a platform router on the SAME zone:
58
+ * Cloudflare 522s a same-zone Worker→Worker public `fetch`). Return a `Fetcher`
59
+ * to route the request through, or null/undefined to fall through to the normal
60
+ * fetch. The SDK names no backend or topology — the CALLER owns BOTH the
61
+ * predicate (which hosts) and the fetcher (the binding). This generalizes
62
+ * `selfBinding` (same-origin → SELF) to any caller policy; both are honored.
63
+ */
64
+ routeSubrequest?: (url: URL, env: TDeps) => Fetcher | null | undefined
47
65
  /**
48
66
  * Optional: handle a request before it reaches the kernel app — e.g. serve a
49
67
  * SPA under `/ui/*` or a same-origin `/api/*` endpoint the view calls. Return
@@ -67,34 +85,114 @@ export interface WorkerEntry<TDeps> {
67
85
  fetch(request: Request, env: TDeps): Response | Promise<Response>
68
86
  }
69
87
 
88
+ /**
89
+ * The request origin as the CLIENT reached it — i.e. the origin a fallback
90
+ * serving URL (and therefore the `iss`) may be derived from. Behind a
91
+ * TLS-terminating proxy (a cloudflared tunnel in front of `wrangler dev`, any
92
+ * reverse proxy) the worker sees plain HTTP, so `request.url` says `http://…`
93
+ * while the public URL is `https://…`; the proxy advertises the original
94
+ * scheme via `X-Forwarded-Proto`. Honoring it is restricted to the http→https
95
+ * UPGRADE of the SAME host (never a downgrade, never a host change), so a
96
+ * spoofed header can at worst derive an `iss` the kernel's JWKS check then
97
+ * fails — it can never make this worker speak for another origin.
98
+ */
99
+ export function clientOrigin(url: URL, request: Request): string {
100
+ if (url.protocol !== 'http:') return url.origin
101
+ const forwarded = request.headers.get('x-forwarded-proto')
102
+ // Multiple proxies append: "https, http" — the first hop is the client-facing one.
103
+ const scheme = forwarded?.split(',')[0]?.trim().toLowerCase()
104
+ return scheme === 'https' ? `https://${url.host}` : url.origin
105
+ }
106
+
107
+ /**
108
+ * Build a `before` hook that serves a static-asset `binding` (e.g. a Workers
109
+ * Assets binding) mounted under `base` (default `/ui`) — the runtime half of a
110
+ * domain's `client` binding. Returns `undefined` for non-matching paths (and
111
+ * when no binding is present) so the request falls through to domain dispatch.
112
+ *
113
+ * Asset URLs are rooted at `base` (a client bundler sets `base: '<base>/'`);
114
+ * this hook strips that prefix before delegating to the binding, so
115
+ * `<base>/x.js` resolves from the binding's root. When `devProxy` yields a URL
116
+ * (local dev), requests are proxied there instead — the seam for a bundler's
117
+ * HMR dev server. Whether unknown sub-paths fall back to `index.html` is the
118
+ * binding's own concern (e.g. wrangler's `not_found_handling`), not baked in
119
+ * here.
120
+ *
121
+ * Lives here, type-checked and testable, instead of being emitted as a string
122
+ * by every adapter's worker codegen.
123
+ */
124
+ export function assets<TDeps>(opts: {
125
+ base?: string
126
+ binding: (env: TDeps) => Fetcher | null | undefined
127
+ devProxy?: (env: TDeps) => string | null | undefined
128
+ }): (env: TDeps, url: URL, request: Request) => Response | Promise<Response> | undefined {
129
+ const base = (opts.base ?? '/ui').replace(/\/+$/, '')
130
+ const prefix = `${base}/`
131
+ return (env, url, request) => {
132
+ const binding = opts.binding(env)
133
+ if (!binding) return undefined
134
+ if (url.pathname !== base && !url.pathname.startsWith(prefix)) return undefined
135
+ const devUrl = opts.devProxy?.(env)
136
+ if (devUrl) {
137
+ const devBase = devUrl.replace(/\/+$/, '')
138
+ return fetch(new Request(`${devBase}${url.pathname}${url.search}`, request))
139
+ }
140
+ // `<base>` → `/`, `<base>/x` → `/x`: serve from the binding's root.
141
+ const stripped = url.pathname.slice(base.length) || '/'
142
+ const rewritten = new URL(stripped + url.search, url.origin)
143
+ return binding.fetch(new Request(rewritten, request))
144
+ }
145
+ }
146
+
70
147
  export function createWorkerEntry<TDeps>(config: WorkerEntryConfig<TDeps>): WorkerEntry<TDeps> {
71
- let cache: { url: string; origin: string; app: App } | null = null
148
+ // Cache the built app per distinct resolved URL plural and bounded. On the
149
+ // request-origin fallback the URL legitimately alternates for one worker
150
+ // (direct http hits vs https-upgraded tunnel hits, workers.dev + custom
151
+ // domain), and a single slot would tear down and rebuild the whole app on
152
+ // every alternation. The bound caps abuse via attacker-minted Host /
153
+ // X-Forwarded-Proto values on that same fallback path.
154
+ const MAX_CACHED_APPS = 4
155
+ const apps = new Map<string, { origin: string; app: App }>()
72
156
  let self: Fetcher | null = null
157
+ let routeEnv: TDeps | null = null
73
158
 
74
159
  function getApp(url: string, env: TDeps): App {
75
- if (cache && cache.url === url) return cache.app
160
+ const cached = apps.get(url)
161
+ if (cached) return cached.app
76
162
  const { app } = createRemoteServer<TDeps>(config.build(url, env))
77
- cache = { url, origin: new URL(url).origin, app }
163
+ if (apps.size >= MAX_CACHED_APPS) {
164
+ const oldest = apps.keys().next().value
165
+ if (oldest !== undefined) apps.delete(oldest)
166
+ }
167
+ apps.set(url, { origin: new URL(url).origin, app })
78
168
  return app
79
169
  }
80
170
 
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) {
171
+ // A Worker can't fetch its own hostname (SELF), nor on Cloudflare — a
172
+ // same-zone hostname routed to another Worker (the `routeSubrequest` case).
173
+ // When either is configured, override `globalThis.fetch` to redirect those
174
+ // subrequests through the caller's fetcher. Workers with neither get no global
175
+ // mutation. (A self-issued credential's JWKS is resolved in-memory by the
176
+ // verifier, not fetched — see `auth/verify.ts` — so no self-JWKS shim is needed.)
177
+ if (config.selfBinding || config.routeSubrequest) {
88
178
  const originalFetch = globalThis.fetch
89
179
  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
180
+ const href = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url
181
+ try {
182
+ const u = new URL(href)
183
+ // same-origin → SELF (a Worker can't fetch its own hostname)
184
+ if (self && apps.size > 0) {
185
+ for (const cached of apps.values()) {
186
+ if (cached.origin === u.origin) return self.fetch(new Request(input, init))
187
+ }
188
+ }
189
+ // caller policy → its fetcher (e.g. a platform router on the same zone)
190
+ if (config.routeSubrequest && routeEnv) {
191
+ const via = config.routeSubrequest(u, routeEnv)
192
+ if (via) return via.fetch(new Request(input, init))
97
193
  }
194
+ } catch {
195
+ // non-absolute URL — fall through to the original fetch
98
196
  }
99
197
  return originalFetch(input, init)
100
198
  }) as typeof fetch
@@ -103,6 +201,7 @@ export function createWorkerEntry<TDeps>(config: WorkerEntryConfig<TDeps>): Work
103
201
  return {
104
202
  async fetch(request: Request, env: TDeps): Promise<Response> {
105
203
  if (config.selfBinding) self ??= config.selfBinding(env) ?? null
204
+ if (config.routeSubrequest) routeEnv = env
106
205
  // Only parse the request URL when a hook actually needs it.
107
206
  const requestUrl = config.before || config.resolveUrl ? new URL(request.url) : null
108
207
  if (config.before && requestUrl) {
@@ -112,7 +211,7 @@ export function createWorkerEntry<TDeps>(config: WorkerEntryConfig<TDeps>): Work
112
211
  if (handled !== undefined) return handled
113
212
  }
114
213
  const raw = config.resolveUrl
115
- ? config.resolveUrl(env, requestUrl!.origin)
214
+ ? config.resolveUrl(env, clientOrigin(requestUrl!, request))
116
215
  : requireEnv(env, 'WORKER_URL', "the worker's public serving URL (its iss identity)")
117
216
  const url = canonicalizeServingUrl(raw)
118
217
  const dispatched = config.rewriteRequest ? config.rewriteRequest(env, request) : request