@astrale-os/sdk 0.1.7 → 0.1.9
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.
- package/dist/cli/run.d.ts +16 -11
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +51 -17
- package/dist/cli/run.js.map +1 -1
- package/dist/define/remote-function.d.ts +14 -10
- package/dist/define/remote-function.d.ts.map +1 -1
- package/dist/define/remote-function.js.map +1 -1
- package/dist/define/view.d.ts +13 -9
- package/dist/define/view.d.ts.map +1 -1
- package/dist/define/view.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/method/context.d.ts +13 -3
- package/dist/method/context.d.ts.map +1 -1
- package/dist/method/index.d.ts +1 -1
- package/dist/method/index.d.ts.map +1 -1
- package/dist/method/single.d.ts +6 -5
- package/dist/method/single.d.ts.map +1 -1
- package/dist/method/single.js.map +1 -1
- package/dist/server/worker-entry.d.ts +40 -0
- package/dist/server/worker-entry.d.ts.map +1 -1
- package/dist/server/worker-entry.js +50 -18
- package/dist/server/worker-entry.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/run.ts +55 -20
- package/src/define/remote-function.ts +14 -9
- package/src/define/view.ts +16 -9
- package/src/index.ts +1 -0
- package/src/method/context.ts +24 -3
- package/src/method/index.ts +1 -1
- package/src/method/single.ts +6 -5
- package/src/server/worker-entry.ts +61 -17
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker-entry.d.ts","sourceRoot":"","sources":["../../src/server/worker-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAMlD,KAAK,OAAO,GAAG;IAAE,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"worker-entry.d.ts","sourceRoot":"","sources":["../../src/server/worker-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAMlD,KAAK,OAAO,GAAG;IAAE,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CAAE,CAAA;AACxE,KAAK,GAAG,GAAG;IAAE,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CAAE,CAAA;AAEpE;yCACyC;AACzC,KAAK,SAAS,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,GAAG,CAAA;CAAE,CAAA;AAE7C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAC1C,CAAC,EAAE,GAAG,EACN,GAAG,EAAE;IACH,IAAI,EAAE,OAAO,GAAG,IAAI,CAAA;IACpB,IAAI,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAA;IACzB,QAAQ,EAAE,KAAK,GAAG,IAAI,CAAA;IACtB,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,KAAK,OAAO,GAAG,IAAI,GAAG,SAAS,CAAA;CACvE,GACA;IAAE,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CAAE,GAAG,IAAI,CAWlE;AAED,MAAM,WAAW,iBAAiB,CAAC,KAAK;IACtC;;;;OAIG;IACH,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,KAAK,kBAAkB,CAAC,KAAK,CAAC,CAAA;IAC7D;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,KAAK,MAAM,CAAA;IAC1D,gFAAgF;IAChF,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,OAAO,GAAG,IAAI,GAAG,SAAS,CAAA;IACxD;;;;;;;;;OASG;IACH,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,KAAK,OAAO,GAAG,IAAI,GAAG,SAAS,CAAA;IACtE;;;;;OAKG;IACH,MAAM,CAAC,EAAE,CACP,GAAG,EAAE,KAAK,EACV,GAAG,EAAE,GAAG,EACR,OAAO,EAAE,OAAO,KACb,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAA;IACzD;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAA;CAC3D;AAED,MAAM,WAAW,WAAW,CAAC,KAAK;IAChC,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CAClE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAM/D;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE;IAClC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,OAAO,GAAG,IAAI,GAAG,SAAS,CAAA;IACnD,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CACrD,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,SAAS,CAiBvF;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,iBAAiB,CAAC,KAAK,CAAC,GAAG,WAAW,CAAC,KAAK,CAAC,CAsE7F"}
|
|
@@ -24,6 +24,43 @@
|
|
|
24
24
|
import { createRemoteServer } from './create';
|
|
25
25
|
import { requireEnv } from './require-env';
|
|
26
26
|
import { canonicalizeServingUrl } from './serving-url';
|
|
27
|
+
/**
|
|
28
|
+
* Choose where an OUTBOUND subrequest to `u` is routed — or `null` to let the
|
|
29
|
+
* real `fetch` handle it. Order matters and same-origin wins FIRST: a call to
|
|
30
|
+
* this worker's OWN serving origin is a self-dispatch, not an edge fetch — and
|
|
31
|
+
* the caller's `routeSubrequest` policy may itself match our own host (e.g. an
|
|
32
|
+
* `isInstanceHost` predicate matches every `*.svc.astrale.ai`, ours included),
|
|
33
|
+
* so it must never get first look at a same-origin call.
|
|
34
|
+
*
|
|
35
|
+
* • same-origin + SELF binding present → the SELF fetcher: a fresh same-script
|
|
36
|
+
* invocation. The normal Cloudflare path — a Worker can't fetch its own
|
|
37
|
+
* hostname over the edge, so it re-enters itself through the binding.
|
|
38
|
+
* • same-origin + NO SELF binding → the cached app, dispatched IN-PROCESS. A
|
|
39
|
+
* Workers-for-Platforms dispatch-namespace tenant can't service-bind to
|
|
40
|
+
* itself (its script name is platform-renamed), so it has no SELF binding; an
|
|
41
|
+
* in-process dispatch reaches the same script with no edge hop and no binding.
|
|
42
|
+
* `App` and the SELF `Fetcher` share the `{ fetch(request) }` shape, so the
|
|
43
|
+
* caller drives both identically.
|
|
44
|
+
* • else, the caller's `routeSubrequest` policy matched → its fetcher.
|
|
45
|
+
* • else → `null`: passthrough to the real network fetch.
|
|
46
|
+
*
|
|
47
|
+
* Pure (no closure over instance state) so the routing decision is unit-testable
|
|
48
|
+
* without standing up a real app.
|
|
49
|
+
*/
|
|
50
|
+
export function selectSubrequestTarget(u, ctx) {
|
|
51
|
+
// Same-origin self-subrequest → the same script (SELF binding, else in-process).
|
|
52
|
+
for (const cached of ctx.apps) {
|
|
53
|
+
if (cached.origin === u.origin)
|
|
54
|
+
return ctx.self ?? cached.app;
|
|
55
|
+
}
|
|
56
|
+
// Caller policy (e.g. a platform router on the same zone) → its fetcher.
|
|
57
|
+
if (ctx.routeSubrequest && ctx.routeEnv) {
|
|
58
|
+
const via = ctx.routeSubrequest(u, ctx.routeEnv);
|
|
59
|
+
if (via)
|
|
60
|
+
return via;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
27
64
|
/**
|
|
28
65
|
* The request origin as the CLIENT reached it — i.e. the origin a fallback
|
|
29
66
|
* serving URL (and therefore the `iss`) may be derived from. Behind a
|
|
@@ -104,31 +141,26 @@ export function createWorkerEntry(config) {
|
|
|
104
141
|
apps.set(url, { origin: new URL(url).origin, app });
|
|
105
142
|
return app;
|
|
106
143
|
}
|
|
107
|
-
// A Worker can't fetch its own hostname
|
|
144
|
+
// A Worker can't fetch its own hostname over the edge, nor — on Cloudflare — a
|
|
108
145
|
// same-zone hostname routed to another Worker (the `routeSubrequest` case).
|
|
109
146
|
// When either is configured, override `globalThis.fetch` to redirect those
|
|
110
|
-
// subrequests through the
|
|
111
|
-
//
|
|
112
|
-
//
|
|
147
|
+
// subrequests through the right target (see `selectSubrequestTarget`). Workers
|
|
148
|
+
// with neither selfBinding nor routeSubrequest get no global mutation. (A
|
|
149
|
+
// self-issued credential's JWKS is resolved in-memory by the verifier, not
|
|
150
|
+
// fetched — see `auth/verify.ts` — so no self-JWKS shim is needed.)
|
|
113
151
|
if (config.selfBinding || config.routeSubrequest) {
|
|
114
152
|
const originalFetch = globalThis.fetch;
|
|
115
153
|
globalThis.fetch = (async (input, init) => {
|
|
116
154
|
const href = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
|
|
117
155
|
try {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// caller policy → its fetcher (e.g. a platform router on the same zone)
|
|
127
|
-
if (config.routeSubrequest && routeEnv) {
|
|
128
|
-
const via = config.routeSubrequest(u, routeEnv);
|
|
129
|
-
if (via)
|
|
130
|
-
return via.fetch(new Request(input, init));
|
|
131
|
-
}
|
|
156
|
+
const target = selectSubrequestTarget(new URL(href), {
|
|
157
|
+
self,
|
|
158
|
+
apps: apps.values(),
|
|
159
|
+
routeEnv,
|
|
160
|
+
routeSubrequest: config.routeSubrequest,
|
|
161
|
+
});
|
|
162
|
+
if (target)
|
|
163
|
+
return target.fetch(new Request(input, init));
|
|
132
164
|
}
|
|
133
165
|
catch {
|
|
134
166
|
// non-absolute URL — fall through to the original fetch
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker-entry.js","sourceRoot":"","sources":["../../src/server/worker-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"worker-entry.js","sourceRoot":"","sources":["../../src/server/worker-entry.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/cli/run.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `astrale-domain` CLI — dev | build | deploy.
|
|
2
|
+
* The `astrale-domain` CLI — dev | build | deploy | publish.
|
|
3
3
|
*
|
|
4
4
|
* Thin by design: it reads `astrale.config.ts`, resolves `envs[<env>] → params`
|
|
5
5
|
* via the adapter, loads the env's secrets file, then drives `adapter.watch`
|
|
@@ -18,14 +18,17 @@
|
|
|
18
18
|
* astrale-domain prod # = deploy prod
|
|
19
19
|
* astrale-domain deploy <env> # any env key
|
|
20
20
|
* astrale-domain build # rebuild spec only (placeholder URL)
|
|
21
|
-
* astrale-domain publish [env] #
|
|
21
|
+
* astrale-domain publish [env] # register the deployed URL in the admin catalog (NO deploy)
|
|
22
22
|
*
|
|
23
|
-
* `publish`
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
23
|
+
* `publish` registers a domain's ALREADY-deployed URL in the admin catalog by
|
|
24
|
+
* SHELLING OUT to the operator CLI (`astrale domain publish --origin --name
|
|
25
|
+
* --public-url`); it does NOT deploy — run `prod` / `deploy <env>` first, or use
|
|
26
|
+
* `deploy --publish` to deploy AND register in one step. Standalone publish
|
|
27
|
+
* defaults the registered address to `https://<origin>` (what every fleet domain
|
|
28
|
+
* serves at); pass `--public-url` for workers.dev / split-host deploys. Auth
|
|
29
|
+
* lives entirely in the operator CLI — this build tool never touches
|
|
30
|
+
* credentials. Requires `astrale` on PATH (the same CLI the deploy footer
|
|
31
|
+
* already points you at for `domain install`).
|
|
29
32
|
*/
|
|
30
33
|
|
|
31
34
|
import { spawn } from 'node:child_process'
|
|
@@ -44,19 +47,21 @@ import { buildProjectSpec } from './spec'
|
|
|
44
47
|
const CONFIG_NAMES = ['astrale.config.ts', 'astrale.config.js', 'astrale.config.mjs']
|
|
45
48
|
|
|
46
49
|
type ParsedArgs = {
|
|
47
|
-
command: 'dev' | 'build' | 'deploy'
|
|
50
|
+
command: 'dev' | 'build' | 'deploy' | 'publish'
|
|
48
51
|
env: string
|
|
49
52
|
watch: boolean
|
|
50
53
|
/** `--port <n>` (dev only) — overrides the env's local dev port. */
|
|
51
54
|
port?: number
|
|
52
55
|
/** `--host <url>` (dev only) — public URL of a tunnel/proxy front: binds 0.0.0.0 + pins WORKER_URL. */
|
|
53
56
|
host?: string
|
|
54
|
-
/**
|
|
57
|
+
/** `--publish` tail-flag on deploy/prod: ALSO register the freshly-deployed URL (deploy + register). */
|
|
55
58
|
publish?: boolean
|
|
56
|
-
/** `--name <slug>` — registry name to publish under (default:
|
|
59
|
+
/** `--name <slug>` — registry name to publish under (default: the origin's first label, e.g. `ai-gateway`). */
|
|
57
60
|
name?: string
|
|
58
61
|
/** `--install-by-default` — mark the published domain for install on every new instance. */
|
|
59
62
|
installByDefault?: boolean
|
|
63
|
+
/** `--public-url <url>` — (publish only) the address the catalog points at (default: `https://<origin>`). */
|
|
64
|
+
publicUrl?: string
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
export async function run(argv: readonly string[]): Promise<number> {
|
|
@@ -131,6 +136,25 @@ export async function run(argv: readonly string[]): Promise<number> {
|
|
|
131
136
|
return 0
|
|
132
137
|
}
|
|
133
138
|
|
|
139
|
+
if (parsed.command === 'publish') {
|
|
140
|
+
// Register the already-deployed URL in the admin catalog — NO deploy. The
|
|
141
|
+
// public address defaults to `https://<origin>` (the canonical custom-domain
|
|
142
|
+
// prod URL — what every fleet domain serves at); `--public-url` overrides it
|
|
143
|
+
// for workers.dev / split-host deploys. No adapter, secrets, or params: this
|
|
144
|
+
// only points the registry at an address the author already deployed.
|
|
145
|
+
const pkg = readPackageMeta(projectDir)
|
|
146
|
+
const name = parsed.name ?? def.origin.split('.')[0] ?? def.origin
|
|
147
|
+
const url = parsed.publicUrl ?? `https://${def.origin}`
|
|
148
|
+
info(`registering ${def.origin} → ${url} in the admin catalog (no deploy)`)
|
|
149
|
+
return await publishToAdmin({
|
|
150
|
+
origin: def.origin,
|
|
151
|
+
name,
|
|
152
|
+
url,
|
|
153
|
+
...(pkg.description ? { description: pkg.description } : {}),
|
|
154
|
+
...(parsed.installByDefault ? { installByDefault: true } : {}),
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
134
158
|
const adapter = def.adapter as DomainAdapter<unknown>
|
|
135
159
|
// CLI param overrides — applied over the env's params here AND re-applied by
|
|
136
160
|
// the config hot-regen path, which re-resolves `adapter.params` from the
|
|
@@ -184,9 +208,10 @@ export async function run(argv: readonly string[]): Promise<number> {
|
|
|
184
208
|
if (parsed.publish) {
|
|
185
209
|
const pkg = readPackageMeta(projectDir)
|
|
186
210
|
// The registry `name` is a short catalog slug, distinct from the FQDN-like
|
|
187
|
-
// `origin
|
|
188
|
-
//
|
|
189
|
-
|
|
211
|
+
// `origin` AND from the npm package name (`@scope/…` for a published domain —
|
|
212
|
+
// illegal as a catalog slug). Default to the origin's first label (the fleet
|
|
213
|
+
// convention, e.g. `ai-gateway`); `--name` overrides.
|
|
214
|
+
const name = parsed.name ?? def.origin.split('.')[0] ?? def.origin
|
|
190
215
|
return await publishToAdmin({
|
|
191
216
|
origin: def.origin,
|
|
192
217
|
name,
|
|
@@ -539,6 +564,7 @@ export function parseArgs(argv: readonly string[]): ParsedArgs {
|
|
|
539
564
|
let port: number | undefined
|
|
540
565
|
let host: string | undefined
|
|
541
566
|
let name: string | undefined
|
|
567
|
+
let publicUrl: string | undefined
|
|
542
568
|
const cleaned: string[] = []
|
|
543
569
|
for (let i = 0; i < rest.length; i++) {
|
|
544
570
|
const a = rest[i]!
|
|
@@ -560,6 +586,12 @@ export function parseArgs(argv: readonly string[]): ParsedArgs {
|
|
|
560
586
|
if (!name) throw new Error('--name needs a value (the registry slug to publish under)')
|
|
561
587
|
} else if (a.startsWith('--name=')) {
|
|
562
588
|
name = a.slice('--name='.length)
|
|
589
|
+
} else if (a === '--public-url') {
|
|
590
|
+
publicUrl = rest[++i]
|
|
591
|
+
if (!publicUrl)
|
|
592
|
+
throw new Error('--public-url needs a value (the address the catalog points at)')
|
|
593
|
+
} else if (a.startsWith('--public-url=')) {
|
|
594
|
+
publicUrl = a.slice('--public-url='.length)
|
|
563
595
|
} else {
|
|
564
596
|
cleaned.push(a)
|
|
565
597
|
}
|
|
@@ -590,13 +622,15 @@ export function parseArgs(argv: readonly string[]): ParsedArgs {
|
|
|
590
622
|
return { command: 'deploy', env, watch, ...(publish ? { publish } : {}), ...pub }
|
|
591
623
|
}
|
|
592
624
|
case 'publish':
|
|
593
|
-
//
|
|
625
|
+
// Register the ALREADY-deployed URL in the admin catalog — NO deploy.
|
|
626
|
+
// (Use `deploy [env] --publish` to deploy AND register in one step.) The
|
|
627
|
+
// public address defaults to `https://<origin>`; `--public-url` overrides.
|
|
594
628
|
return {
|
|
595
|
-
command: '
|
|
629
|
+
command: 'publish',
|
|
596
630
|
env: positionals[0] ?? 'prod',
|
|
597
631
|
watch: false,
|
|
598
|
-
publish: true,
|
|
599
632
|
...pub,
|
|
633
|
+
...(publicUrl !== undefined ? { publicUrl } : {}),
|
|
600
634
|
}
|
|
601
635
|
case 'build':
|
|
602
636
|
// Spec-only — no env resolution happens on this path.
|
|
@@ -664,11 +698,12 @@ function printUsage(msg?: string): void {
|
|
|
664
698
|
` astrale-domain dev # deploy dev --watch (hot-reload, prints URL)\n` +
|
|
665
699
|
` astrale-domain prod # deploy prod\n` +
|
|
666
700
|
` astrale-domain deploy <env> # deploy any env key (--watch optional)\n` +
|
|
667
|
-
` astrale-domain publish [env] #
|
|
701
|
+
` astrale-domain publish [env] # register the already-deployed URL in the admin catalog (NO deploy)\n` +
|
|
668
702
|
` astrale-domain build # rebuild the diagnostic spec only\n` +
|
|
669
703
|
`\nFlags:\n` +
|
|
670
|
-
` --publish # on deploy/prod: register the deployed URL (
|
|
671
|
-
` --name <slug> # registry name to publish under (default:
|
|
704
|
+
` --publish # on deploy/prod: ALSO register the deployed URL (deploy + register)\n` +
|
|
705
|
+
` --name <slug> # registry name to publish under (default: the origin's first label)\n` +
|
|
706
|
+
` --public-url <url> # (publish) address the catalog points at (default: https://<origin>)\n` +
|
|
672
707
|
` --install-by-default # mark the published domain for install on every new instance\n` +
|
|
673
708
|
`\n publish shells out to \`astrale domain publish\` (needs \`astrale\` on PATH).\n\n`,
|
|
674
709
|
)
|
|
@@ -19,24 +19,28 @@
|
|
|
19
19
|
import type { AuthPolicy, FunctionBinding } from '@astrale-os/kernel-api/routed'
|
|
20
20
|
import type { FnMap } from '@astrale-os/kernel-client'
|
|
21
21
|
import type { BoundClientSessionView } from '@astrale-os/kernel-client/session'
|
|
22
|
-
import type { AuthContext } from '@astrale-os/kernel-core'
|
|
23
22
|
import type { Context } from 'hono'
|
|
24
23
|
import type { z } from 'zod'
|
|
25
24
|
|
|
26
25
|
import type { CallRemoteFn } from '../dispatch/call-remote'
|
|
27
|
-
import type { KernelForAuth } from '../method/context'
|
|
26
|
+
import type { AuthForPolicy, KernelForAuth } from '../method/context'
|
|
28
27
|
|
|
29
28
|
export type RemoteFunctionContext<
|
|
30
29
|
TParams,
|
|
31
30
|
TDeps = unknown,
|
|
32
31
|
TKernel = BoundClientSessionView<FnMap> | null,
|
|
32
|
+
TAuth extends AuthPolicy = AuthPolicy,
|
|
33
33
|
> = {
|
|
34
34
|
/** Validated params (Zod-checked against `inputSchema`). */
|
|
35
35
|
params: TParams
|
|
36
36
|
/** Hono request context — escape hatch for headers, raw body, etc. */
|
|
37
37
|
c: Context
|
|
38
|
-
/**
|
|
39
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Resolved auth context. Its nullability follows the function's `auth`
|
|
40
|
+
* policy: non-null for the default `'required'`, `... | null` for
|
|
41
|
+
* `'optional'`, and `null` for `'public'`.
|
|
42
|
+
*/
|
|
43
|
+
auth: AuthForPolicy<TAuth>
|
|
40
44
|
/** Typed dependency container injected at server startup. */
|
|
41
45
|
deps: TDeps
|
|
42
46
|
/**
|
|
@@ -96,18 +100,19 @@ export type RemoteFunctionDef<
|
|
|
96
100
|
binding?: FunctionBinding
|
|
97
101
|
/**
|
|
98
102
|
* Authentication policy. Defaults to `'required'`. Captured as a literal type
|
|
99
|
-
* so it drives {@link KernelForAuth} on the
|
|
100
|
-
* omit it (or `'required'`)
|
|
101
|
-
* `'
|
|
103
|
+
* so it drives `ctx.auth` and {@link KernelForAuth} on the
|
|
104
|
+
* `execute`/`authorize` context: omit it (or `'required'`) makes both
|
|
105
|
+
* non-null; `'optional'` widens them to `... | null`; `'public'` makes them
|
|
106
|
+
* `null` (webhooks reach the graph via `ctx.selfKernel`).
|
|
102
107
|
*/
|
|
103
108
|
auth?: TAuth
|
|
104
109
|
/** Optional pre-execute authorization. Throw to deny. */
|
|
105
110
|
authorize?: (
|
|
106
|
-
ctx: RemoteFunctionContext<TParams, TDeps, KernelForAuth<TAuth
|
|
111
|
+
ctx: RemoteFunctionContext<TParams, TDeps, KernelForAuth<TAuth>, TAuth>,
|
|
107
112
|
) => void | Promise<void>
|
|
108
113
|
/** The function body. May be async. */
|
|
109
114
|
execute: (
|
|
110
|
-
ctx: RemoteFunctionContext<TParams, TDeps, KernelForAuth<TAuth
|
|
115
|
+
ctx: RemoteFunctionContext<TParams, TDeps, KernelForAuth<TAuth>, TAuth>,
|
|
111
116
|
) => TResult | Promise<TResult>
|
|
112
117
|
/** Optional human-readable description. */
|
|
113
118
|
description?: string
|
package/src/define/view.ts
CHANGED
|
@@ -21,11 +21,12 @@
|
|
|
21
21
|
import type { AuthPolicy, FunctionBinding } from '@astrale-os/kernel-api/routed'
|
|
22
22
|
import type { FnMap } from '@astrale-os/kernel-client'
|
|
23
23
|
import type { BoundClientSessionView } from '@astrale-os/kernel-client/session'
|
|
24
|
-
import type { AuthContext } from '@astrale-os/kernel-core'
|
|
25
24
|
import type { EdgeEndpoint } from '@astrale-os/kernel-dsl'
|
|
26
25
|
import type { Context } from 'hono'
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
import type { AuthForPolicy } from '../method/context'
|
|
28
|
+
|
|
29
|
+
export type ViewRenderContext<TDeps = unknown, TAuth extends AuthPolicy = AuthPolicy> = {
|
|
29
30
|
/** Hono request context — read params, headers, query, etc. */
|
|
30
31
|
c: Context
|
|
31
32
|
/**
|
|
@@ -34,8 +35,12 @@ export type ViewRenderContext<TDeps = unknown> = {
|
|
|
34
35
|
* the binding has no placeholders.
|
|
35
36
|
*/
|
|
36
37
|
params: Record<string, string>
|
|
37
|
-
/**
|
|
38
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Resolved auth context. Its nullability follows the View's `auth` policy:
|
|
40
|
+
* non-null for `'required'`, `... | null` for `'optional'`, and `null` for
|
|
41
|
+
* `'public'`.
|
|
42
|
+
*/
|
|
43
|
+
auth: AuthForPolicy<TAuth>
|
|
39
44
|
/** Typed dependency container injected at server startup. */
|
|
40
45
|
deps: TDeps
|
|
41
46
|
/**
|
|
@@ -48,7 +53,7 @@ export type ViewRenderContext<TDeps = unknown> = {
|
|
|
48
53
|
selfKernel: (kernelUrl?: string) => Promise<BoundClientSessionView<FnMap>>
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
export type ViewDef<TDeps = unknown> = {
|
|
56
|
+
export type ViewDef<TDeps = unknown, TAuth extends AuthPolicy = AuthPolicy> = {
|
|
52
57
|
/**
|
|
53
58
|
* Override the binding (URL + route shape). When absent, SDK defaults to
|
|
54
59
|
* `{ remoteUrl: ${url}/<viewsFolder>/<slug> }`. The HTTP verb (GET for
|
|
@@ -68,18 +73,18 @@ export type ViewDef<TDeps = unknown> = {
|
|
|
68
73
|
*/
|
|
69
74
|
mount?: string
|
|
70
75
|
/** Authentication policy. Defaults to `'required'`. */
|
|
71
|
-
auth?:
|
|
76
|
+
auth?: TAuth
|
|
72
77
|
/**
|
|
73
78
|
* Optional pre-render authorization. Runs after auth resolution; throw to
|
|
74
79
|
* deny. SDK wraps as 403.
|
|
75
80
|
*/
|
|
76
|
-
authorize?: (ctx: ViewRenderContext<TDeps>) => void | Promise<void>
|
|
81
|
+
authorize?: (ctx: ViewRenderContext<TDeps, TAuth>) => void | Promise<void>
|
|
77
82
|
/**
|
|
78
83
|
* Render the iframe response. May return a redirect, a proxy, or inline
|
|
79
84
|
* HTML. When omitted, no worker route is mounted — the binding URL
|
|
80
85
|
* points elsewhere (CDN, external service).
|
|
81
86
|
*/
|
|
82
|
-
render?: (ctx: ViewRenderContext<TDeps>) => Response | Promise<Response>
|
|
87
|
+
render?: (ctx: ViewRenderContext<TDeps, TAuth>) => Response | Promise<Response>
|
|
83
88
|
/**
|
|
84
89
|
* Optional target(s) the View attaches to via `view_for` edge(s). Typically
|
|
85
90
|
* `selfOf(SomeClass)` to bind to a class meta-node, or a `CorePath`
|
|
@@ -96,6 +101,8 @@ export type ViewDef<TDeps = unknown> = {
|
|
|
96
101
|
* `defineRemoteDomain` consumes the typed shape and the author retains
|
|
97
102
|
* full type inference on `render` / `authorize`.
|
|
98
103
|
*/
|
|
99
|
-
export function defineView<TDeps = unknown
|
|
104
|
+
export function defineView<TDeps = unknown, TAuth extends AuthPolicy = 'required'>(
|
|
105
|
+
def: ViewDef<TDeps, TAuth>,
|
|
106
|
+
): ViewDef<TDeps, TAuth> {
|
|
100
107
|
return def
|
|
101
108
|
}
|
package/src/index.ts
CHANGED
package/src/method/context.ts
CHANGED
|
@@ -43,11 +43,32 @@ export type KernelForAuth<TAuth extends AuthPolicy> = TAuth extends 'public'
|
|
|
43
43
|
? Kernel | null
|
|
44
44
|
: Kernel
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
/**
|
|
47
|
+
* The resolved auth context a handler receives, derived from its declared
|
|
48
|
+
* `auth` policy. Mirrors {@link KernelForAuth}: required methods have a
|
|
49
|
+
* verified auth context, optional/public methods encode the nullable cases.
|
|
50
|
+
*/
|
|
51
|
+
export type AuthForPolicy<TAuth extends AuthPolicy> = TAuth extends 'public'
|
|
52
|
+
? null
|
|
53
|
+
: TAuth extends 'optional'
|
|
54
|
+
? AuthContext | null
|
|
55
|
+
: AuthContext
|
|
56
|
+
|
|
57
|
+
export type RemoteContext<
|
|
58
|
+
TParams,
|
|
59
|
+
TSelf,
|
|
60
|
+
TDeps,
|
|
61
|
+
TKernel = Kernel | null,
|
|
62
|
+
TAuth extends AuthPolicy = AuthPolicy,
|
|
63
|
+
> = {
|
|
47
64
|
/** Validated params (Zod-checked against the method's `inputSchema`). */
|
|
48
65
|
params: TParams
|
|
49
|
-
/**
|
|
50
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Auth context resolved from the inbound delegation credential. Its
|
|
68
|
+
* nullability follows the method's `auth` policy: non-null for the default
|
|
69
|
+
* `'required'`, `... | null` for `'optional'`, and `null` for `'public'`.
|
|
70
|
+
*/
|
|
71
|
+
auth: AuthForPolicy<TAuth>
|
|
51
72
|
/** Bound node instance — `undefined` for static methods. */
|
|
52
73
|
self: TSelf
|
|
53
74
|
/** Typed dependency container injected at server startup. */
|
package/src/method/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { Kernel, KernelForAuth, RemoteContext } from './context'
|
|
1
|
+
export type { AuthForPolicy, Kernel, KernelForAuth, RemoteContext } from './context'
|
|
2
2
|
export type { RemoteHandler, AnyRemoteHandler, MethodImpl } from './single'
|
|
3
3
|
export { remoteMethod } from './single'
|
|
4
4
|
export type { ClassMethodsImpl, InterfaceMethodsImpl, SchemaMethodsImpl } from './class'
|
package/src/method/single.ts
CHANGED
|
@@ -38,7 +38,7 @@ import type { KernelForAuth, RemoteContext } from './context'
|
|
|
38
38
|
export type RemoteHandler<TParams, TResult, TSelf, TDeps, TAuth extends AuthPolicy = 'required'> = {
|
|
39
39
|
/** The handler body. May be async or an async generator (for `output: 'stream'`). */
|
|
40
40
|
execute?: (
|
|
41
|
-
ctx: RemoteContext<TParams, TSelf, TDeps, KernelForAuth<TAuth
|
|
41
|
+
ctx: RemoteContext<TParams, TSelf, TDeps, KernelForAuth<TAuth>, TAuth>,
|
|
42
42
|
) => TResult | Promise<TResult> | AsyncGenerator<TResult>
|
|
43
43
|
/**
|
|
44
44
|
* Optional REST binding — attaches a native HTTP route to this method.
|
|
@@ -55,9 +55,10 @@ export type RemoteHandler<TParams, TResult, TSelf, TDeps, TAuth extends AuthPoli
|
|
|
55
55
|
description?: string
|
|
56
56
|
/**
|
|
57
57
|
* Authentication policy. Defaults to `'required'` when absent. Captured as a
|
|
58
|
-
* literal type so it drives {@link KernelForAuth} on the
|
|
59
|
-
* context: omit it (or set `'required'`) and
|
|
60
|
-
* `
|
|
58
|
+
* literal type so it drives `ctx.auth` and {@link KernelForAuth} on the
|
|
59
|
+
* `execute`/`authorize` context: omit it (or set `'required'`) and both
|
|
60
|
+
* `ctx.auth` and `ctx.kernel` are non-null; `'optional'` widens them to
|
|
61
|
+
* `... | null`; `'public'` makes both `null`.
|
|
61
62
|
*/
|
|
62
63
|
auth?: TAuth
|
|
63
64
|
/**
|
|
@@ -75,7 +76,7 @@ export type RemoteHandler<TParams, TResult, TSelf, TDeps, TAuth extends AuthPoli
|
|
|
75
76
|
* additive ergonomic gating on top, not a replacement.
|
|
76
77
|
*/
|
|
77
78
|
authorize?: (
|
|
78
|
-
ctx: RemoteContext<TParams, TSelf, TDeps, KernelForAuth<TAuth
|
|
79
|
+
ctx: RemoteContext<TParams, TSelf, TDeps, KernelForAuth<TAuth>, TAuth>,
|
|
79
80
|
) => void | Promise<void>
|
|
80
81
|
}
|
|
81
82
|
|
|
@@ -31,6 +31,54 @@ import { canonicalizeServingUrl } from './serving-url'
|
|
|
31
31
|
type Fetcher = { fetch(request: Request): Response | Promise<Response> }
|
|
32
32
|
type App = { fetch(request: Request): Response | Promise<Response> }
|
|
33
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
|
+
|
|
34
82
|
export interface WorkerEntryConfig<TDeps> {
|
|
35
83
|
/**
|
|
36
84
|
* Build the `createRemoteServer` config for the resolved serving `url`. Called
|
|
@@ -152,7 +200,7 @@ export function createWorkerEntry<TDeps>(config: WorkerEntryConfig<TDeps>): Work
|
|
|
152
200
|
// every alternation. The bound caps abuse via attacker-minted Host /
|
|
153
201
|
// X-Forwarded-Proto values on that same fallback path.
|
|
154
202
|
const MAX_CACHED_APPS = 4
|
|
155
|
-
const apps = new Map<string,
|
|
203
|
+
const apps = new Map<string, CachedApp>()
|
|
156
204
|
let self: Fetcher | null = null
|
|
157
205
|
let routeEnv: TDeps | null = null
|
|
158
206
|
|
|
@@ -168,29 +216,25 @@ export function createWorkerEntry<TDeps>(config: WorkerEntryConfig<TDeps>): Work
|
|
|
168
216
|
return app
|
|
169
217
|
}
|
|
170
218
|
|
|
171
|
-
// A Worker can't fetch its own hostname
|
|
219
|
+
// A Worker can't fetch its own hostname over the edge, nor — on Cloudflare — a
|
|
172
220
|
// same-zone hostname routed to another Worker (the `routeSubrequest` case).
|
|
173
221
|
// When either is configured, override `globalThis.fetch` to redirect those
|
|
174
|
-
// subrequests through the
|
|
175
|
-
//
|
|
176
|
-
//
|
|
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.)
|
|
177
226
|
if (config.selfBinding || config.routeSubrequest) {
|
|
178
227
|
const originalFetch = globalThis.fetch
|
|
179
228
|
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
180
229
|
const href = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url
|
|
181
230
|
try {
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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))
|
|
193
|
-
}
|
|
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))
|
|
194
238
|
} catch {
|
|
195
239
|
// non-absolute URL — fall through to the original fetch
|
|
196
240
|
}
|