@astrale-os/sdk 0.4.12 → 0.4.14

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 (190) hide show
  1. package/dist/cli/run.d.ts +8 -2
  2. package/dist/cli/run.d.ts.map +1 -1
  3. package/dist/cli/run.js +51 -1
  4. package/dist/cli/run.js.map +1 -1
  5. package/dist/define/remote-function.d.ts +3 -1
  6. package/dist/define/remote-function.d.ts.map +1 -1
  7. package/dist/define/remote-function.js.map +1 -1
  8. package/dist/dispatch/dispatcher.d.ts +10 -20
  9. package/dist/dispatch/dispatcher.d.ts.map +1 -1
  10. package/dist/dispatch/dispatcher.js +55 -54
  11. package/dist/dispatch/dispatcher.js.map +1 -1
  12. package/dist/dispatch/execute.d.ts +2 -1
  13. package/dist/dispatch/execute.d.ts.map +1 -1
  14. package/dist/dispatch/execute.js +1 -0
  15. package/dist/dispatch/execute.js.map +1 -1
  16. package/dist/domain/extend-functions.d.ts.map +1 -1
  17. package/dist/domain/extend-functions.js +2 -1
  18. package/dist/domain/extend-functions.js.map +1 -1
  19. package/dist/index.d.ts +3 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +2 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/linter/analyze.d.ts +6 -0
  24. package/dist/linter/analyze.d.ts.map +1 -0
  25. package/dist/linter/analyze.js +12 -0
  26. package/dist/linter/analyze.js.map +1 -0
  27. package/dist/linter/diagnostic.d.ts +32 -0
  28. package/dist/linter/diagnostic.d.ts.map +1 -0
  29. package/dist/linter/diagnostic.js +35 -0
  30. package/dist/linter/diagnostic.js.map +1 -0
  31. package/dist/linter/index.d.ts +10 -0
  32. package/dist/linter/index.d.ts.map +1 -0
  33. package/dist/linter/index.js +6 -0
  34. package/dist/linter/index.js.map +1 -0
  35. package/dist/linter/lint.d.ts +8 -0
  36. package/dist/linter/lint.d.ts.map +1 -0
  37. package/dist/linter/lint.js +26 -0
  38. package/dist/linter/lint.js.map +1 -0
  39. package/dist/linter/oxlint/config.d.ts +14 -0
  40. package/dist/linter/oxlint/config.d.ts.map +1 -0
  41. package/dist/linter/oxlint/config.js +18 -0
  42. package/dist/linter/oxlint/config.js.map +1 -0
  43. package/dist/linter/oxlint/plugin.d.ts +13 -0
  44. package/dist/linter/oxlint/plugin.d.ts.map +1 -0
  45. package/dist/linter/oxlint/plugin.js +14 -0
  46. package/dist/linter/oxlint/plugin.js.map +1 -0
  47. package/dist/linter/oxlint/run.d.ts +7 -0
  48. package/dist/linter/oxlint/run.d.ts.map +1 -0
  49. package/dist/linter/oxlint/run.js +122 -0
  50. package/dist/linter/oxlint/run.js.map +1 -0
  51. package/dist/linter/preflight.d.ts +2 -0
  52. package/dist/linter/preflight.d.ts.map +1 -0
  53. package/dist/linter/preflight.js +162 -0
  54. package/dist/linter/preflight.js.map +1 -0
  55. package/dist/linter/project.d.ts +18 -0
  56. package/dist/linter/project.d.ts.map +1 -0
  57. package/dist/linter/project.js +150 -0
  58. package/dist/linter/project.js.map +1 -0
  59. package/dist/linter/report.d.ts +4 -0
  60. package/dist/linter/report.d.ts.map +1 -0
  61. package/dist/linter/report.js +22 -0
  62. package/dist/linter/report.js.map +1 -0
  63. package/dist/linter/rules/catalog.d.ts +56 -0
  64. package/dist/linter/rules/catalog.d.ts.map +1 -0
  65. package/dist/linter/rules/catalog.js +53 -0
  66. package/dist/linter/rules/catalog.js.map +1 -0
  67. package/dist/linter/rules/core-has-no-async.d.ts +3 -0
  68. package/dist/linter/rules/core-has-no-async.d.ts.map +1 -0
  69. package/dist/linter/rules/core-has-no-async.js +43 -0
  70. package/dist/linter/rules/core-has-no-async.js.map +1 -0
  71. package/dist/linter/rules/core-is-pure.d.ts +3 -0
  72. package/dist/linter/rules/core-is-pure.d.ts.map +1 -0
  73. package/dist/linter/rules/core-is-pure.js +74 -0
  74. package/dist/linter/rules/core-is-pure.js.map +1 -0
  75. package/dist/linter/rules/no-nested-steps.d.ts +3 -0
  76. package/dist/linter/rules/no-nested-steps.d.ts.map +1 -0
  77. package/dist/linter/rules/no-nested-steps.js +32 -0
  78. package/dist/linter/rules/no-nested-steps.js.map +1 -0
  79. package/dist/linter/rules/no-read-inside-mutate.d.ts +3 -0
  80. package/dist/linter/rules/no-read-inside-mutate.d.ts.map +1 -0
  81. package/dist/linter/rules/no-read-inside-mutate.js +64 -0
  82. package/dist/linter/rules/no-read-inside-mutate.js.map +1 -0
  83. package/dist/linter/rules/one-remote-definition-per-file.d.ts +3 -0
  84. package/dist/linter/rules/one-remote-definition-per-file.d.ts.map +1 -0
  85. package/dist/linter/rules/one-remote-definition-per-file.js +23 -0
  86. package/dist/linter/rules/one-remote-definition-per-file.js.map +1 -0
  87. package/dist/linter/rules/oxlint-ast.d.ts +30 -0
  88. package/dist/linter/rules/oxlint-ast.d.ts.map +1 -0
  89. package/dist/linter/rules/oxlint-ast.js +104 -0
  90. package/dist/linter/rules/oxlint-ast.js.map +1 -0
  91. package/dist/linter/rules/oxlint-rule.d.ts +45 -0
  92. package/dist/linter/rules/oxlint-rule.d.ts.map +1 -0
  93. package/dist/linter/rules/oxlint-rule.js +2 -0
  94. package/dist/linter/rules/oxlint-rule.js.map +1 -0
  95. package/dist/linter/rules/project-rule.d.ts +10 -0
  96. package/dist/linter/rules/project-rule.d.ts.map +1 -0
  97. package/dist/linter/rules/project-rule.js +24 -0
  98. package/dist/linter/rules/project-rule.js.map +1 -0
  99. package/dist/linter/rules/thin-schema-composition-root.d.ts +3 -0
  100. package/dist/linter/rules/thin-schema-composition-root.d.ts.map +1 -0
  101. package/dist/linter/rules/thin-schema-composition-root.js +23 -0
  102. package/dist/linter/rules/thin-schema-composition-root.js.map +1 -0
  103. package/dist/linter/source.d.ts +24 -0
  104. package/dist/linter/source.d.ts.map +1 -0
  105. package/dist/linter/source.js +279 -0
  106. package/dist/linter/source.js.map +1 -0
  107. package/dist/linter/suppression.d.ts +3 -0
  108. package/dist/linter/suppression.d.ts.map +1 -0
  109. package/dist/linter/suppression.js +11 -0
  110. package/dist/linter/suppression.js.map +1 -0
  111. package/dist/method/context.d.ts +3 -1
  112. package/dist/method/context.d.ts.map +1 -1
  113. package/dist/server/auxiliary-routes.d.ts +5 -1
  114. package/dist/server/auxiliary-routes.d.ts.map +1 -1
  115. package/dist/server/auxiliary-routes.js +19 -10
  116. package/dist/server/auxiliary-routes.js.map +1 -1
  117. package/dist/server/create.js +1 -1
  118. package/dist/server/create.js.map +1 -1
  119. package/dist/server/index.d.ts +4 -2
  120. package/dist/server/index.d.ts.map +1 -1
  121. package/dist/server/index.js +2 -1
  122. package/dist/server/index.js.map +1 -1
  123. package/dist/server/service-entry.d.ts +32 -0
  124. package/dist/server/service-entry.d.ts.map +1 -0
  125. package/dist/server/service-entry.js +150 -0
  126. package/dist/server/service-entry.js.map +1 -0
  127. package/dist/server/start.d.ts.map +1 -1
  128. package/dist/server/start.js.map +1 -1
  129. package/dist/server/worker-entry.d.ts +7 -3
  130. package/dist/server/worker-entry.d.ts.map +1 -1
  131. package/dist/server/worker-entry.js +7 -1
  132. package/dist/server/worker-entry.js.map +1 -1
  133. package/dist/service/functions.d.ts +148 -0
  134. package/dist/service/functions.d.ts.map +1 -0
  135. package/dist/service/functions.js +115 -0
  136. package/dist/service/functions.js.map +1 -0
  137. package/dist/service/index.d.ts +3 -0
  138. package/dist/service/index.d.ts.map +1 -0
  139. package/dist/service/index.js +2 -0
  140. package/dist/service/index.js.map +1 -0
  141. package/package.json +27 -7
  142. package/src/cli/run.ts +53 -2
  143. package/src/define/remote-function.ts +3 -1
  144. package/src/dispatch/dispatcher.ts +62 -69
  145. package/src/dispatch/execute.ts +3 -1
  146. package/src/domain/extend-functions.ts +5 -1
  147. package/src/index.ts +20 -2
  148. package/src/linter/analyze.ts +17 -0
  149. package/src/linter/diagnostic.ts +68 -0
  150. package/src/linter/docs/DIAGNOSTICS.md +47 -0
  151. package/src/linter/docs/JUDGMENT.md +81 -0
  152. package/src/linter/docs/PRINCIPLES.md +97 -0
  153. package/src/linter/docs/README.md +38 -0
  154. package/src/linter/docs/RULE-AUTHORING.md +144 -0
  155. package/src/linter/docs/RULES.md +153 -0
  156. package/src/linter/index.ts +14 -0
  157. package/src/linter/lint.ts +32 -0
  158. package/src/linter/oxlint/astrale-ox.d.ts +5 -0
  159. package/src/linter/oxlint/config.ts +19 -0
  160. package/src/linter/oxlint/plugin.js +16 -0
  161. package/src/linter/oxlint/run.ts +162 -0
  162. package/src/linter/preflight.ts +181 -0
  163. package/src/linter/project.ts +190 -0
  164. package/src/linter/report.ts +25 -0
  165. package/src/linter/rules/catalog.ts +67 -0
  166. package/src/linter/rules/core-has-no-async.js +43 -0
  167. package/src/linter/rules/core-is-pure.ts +103 -0
  168. package/src/linter/rules/no-nested-steps.js +37 -0
  169. package/src/linter/rules/no-read-inside-mutate.js +72 -0
  170. package/src/linter/rules/one-remote-definition-per-file.ts +29 -0
  171. package/src/linter/rules/oxlint-ast.js +110 -0
  172. package/src/linter/rules/oxlint-rule.ts +35 -0
  173. package/src/linter/rules/project-rule.ts +37 -0
  174. package/src/linter/rules/thin-schema-composition-root.ts +26 -0
  175. package/src/linter/source.ts +323 -0
  176. package/src/linter/suppression.ts +11 -0
  177. package/src/method/context.ts +3 -1
  178. package/src/server/auxiliary-routes.ts +43 -11
  179. package/src/server/create.ts +1 -1
  180. package/src/server/index.ts +10 -2
  181. package/src/server/service-entry.ts +209 -0
  182. package/src/server/start.ts +2 -1
  183. package/src/server/worker-entry.ts +18 -5
  184. package/src/service/functions.ts +164 -0
  185. package/src/service/index.ts +18 -0
  186. package/dist/defer/index.d.ts +0 -24
  187. package/dist/defer/index.d.ts.map +0 -1
  188. package/dist/defer/index.js +0 -42
  189. package/dist/defer/index.js.map +0 -1
  190. package/src/defer/index.ts +0 -64
@@ -0,0 +1,209 @@
1
+ import type { FunctionBinding } from '@astrale-os/kernel-api/routed'
2
+ import type { Node } from '@astrale-os/kernel-core'
3
+ import type { CorsConfig } from '@astrale-os/kernel-server'
4
+
5
+ import { K } from '@astrale-os/kernel-core'
6
+ import { Hono } from 'hono'
7
+
8
+ import type { RemoteIdentityConfig } from '../auth/identity.js'
9
+ import type { AnyRemoteFunctionDef } from '../define/remote-function.js'
10
+ import type { Fetcher, WorkerEntry } from './worker-entry.js'
11
+
12
+ import { makeFunctionContext } from '../auth/function-context.js'
13
+ import { buildFunctionSchemas, DEFAULT_FUNCTIONS_FOLDER } from '../domain/extend-functions.js'
14
+ import {
15
+ SERVICE_FUNCTIONS_PATH,
16
+ ServiceFunctionDiscoveryRequestSchema,
17
+ signServiceFunctionManifest,
18
+ toServiceFunctionManifestEntries,
19
+ } from '../service/functions.js'
20
+ import { mountAuxiliaryRoutes } from './auxiliary-routes.js'
21
+ import { createAppWorkerEntry } from './worker-entry.js'
22
+
23
+ export interface ServiceWorkerIdentityEnv {
24
+ IDENTITY_ISS?: string
25
+ IDENTITY_SUB?: string
26
+ IDENTITY_PRIVATE_KEY?: string
27
+ ASTRALE_KERNEL_AUDIENCE?: string
28
+ }
29
+
30
+ export interface ServiceWorkerEntryConfig<TEnv extends ServiceWorkerIdentityEnv> {
31
+ functions?: Record<string, AnyRemoteFunctionDef>
32
+ functionsFolder?: string
33
+ cors?: CorsConfig
34
+ resolveUrl?: (env: TEnv, requestOrigin: string) => string
35
+ selfBinding?: (env: TEnv) => Fetcher | null | undefined
36
+ routeSubrequest?: (url: URL, env: TEnv) => Fetcher | null | undefined
37
+ }
38
+
39
+ /** A Service worker with zero or more first-class kernel Functions.
40
+ *
41
+ * The function map is the only declaration: it drives both HTTP routes and the
42
+ * signed deploy-time manifest. Function identities are read lazily from the
43
+ * graph after Services has reconciled them, while every Function reuses the
44
+ * Service's one private signing key. */
45
+ export function serviceWorkerEntry<TEnv extends ServiceWorkerIdentityEnv>(
46
+ config: ServiceWorkerEntryConfig<TEnv>,
47
+ ): WorkerEntry<TEnv> {
48
+ const functions = config.functions ?? {}
49
+ const functionsFolder = config.functionsFolder ?? DEFAULT_FUNCTIONS_FOLDER
50
+
51
+ return createAppWorkerEntry<TEnv>({
52
+ buildApp: (url, env) => {
53
+ const app = new Hono()
54
+ const serviceIdentity = identityFromEnv(env)
55
+ const { schemas, bindings } = buildFunctionSchemas(functions, url, functionsFolder)
56
+ assertLocalBindings(url, bindings)
57
+ const resolveFunctionIdentity = createFunctionIdentityResolver(env, serviceIdentity)
58
+
59
+ app.get('/', (c) => c.json({ ok: true, kind: 'astrale-service' }))
60
+ app.post(SERVICE_FUNCTIONS_PATH, async (c) => {
61
+ const parsed = ServiceFunctionDiscoveryRequestSchema.safeParse(
62
+ await c.req.json().catch(() => null),
63
+ )
64
+ if (!parsed.success) {
65
+ return c.json({ error: 'Invalid service function discovery request' }, 400)
66
+ }
67
+ const response = await signServiceFunctionManifest(
68
+ {
69
+ version: 1,
70
+ service: {
71
+ issuer: serviceIdentity.issuer,
72
+ subject: serviceIdentity.subject,
73
+ },
74
+ functions: toServiceFunctionManifestEntries(schemas),
75
+ },
76
+ parsed.data,
77
+ serviceIdentity,
78
+ )
79
+ return c.json(response)
80
+ })
81
+
82
+ mountAuxiliaryRoutes({
83
+ app,
84
+ url,
85
+ remoteFunctions: functions,
86
+ remoteFunctionBindings: bindings,
87
+ deps: env,
88
+ resolveIdentity: (kind, slug) => {
89
+ if (kind !== 'remoteFunction') {
90
+ throw new Error(`serviceWorkerEntry cannot resolve ${kind} identity ${slug}`)
91
+ }
92
+ return resolveFunctionIdentity(slug)
93
+ },
94
+ cors: config.cors ?? { origin: '*' },
95
+ })
96
+ return app
97
+ },
98
+ resolveUrl: config.resolveUrl ?? ((_env, requestOrigin) => requestOrigin),
99
+ selfBinding: config.selfBinding,
100
+ routeSubrequest: config.routeSubrequest,
101
+ })
102
+ }
103
+
104
+ function identityFromEnv<TEnv extends ServiceWorkerIdentityEnv>(env: TEnv): RemoteIdentityConfig {
105
+ return {
106
+ issuer: required(env, 'IDENTITY_ISS'),
107
+ subject: required(env, 'IDENTITY_SUB'),
108
+ privateKey: parsePrivateKey(required(env, 'IDENTITY_PRIVATE_KEY')),
109
+ }
110
+ }
111
+
112
+ function createFunctionIdentityResolver<TEnv extends ServiceWorkerIdentityEnv>(
113
+ env: TEnv,
114
+ serviceIdentity: RemoteIdentityConfig,
115
+ ): (slug: string) => Promise<RemoteIdentityConfig> {
116
+ const kernelUrl = required(env, 'ASTRALE_KERNEL_AUDIENCE')
117
+ let cachedAt = 0
118
+ let identities = new Map<string, { issuer: string; subject: string }>()
119
+ const ttlMs = 60_000
120
+
121
+ return async (slug) => {
122
+ if (Date.now() - cachedAt >= ttlMs || !identities.has(slug)) {
123
+ const kernel = await makeFunctionContext(serviceIdentity, env, {
124
+ ref: 'service',
125
+ defaultKernelUrl: kernelUrl,
126
+ }).kernel()
127
+ const service = await kernel.get(`@${serviceIdentity.subject}`)
128
+ if (!service) throw new Error(`Service @${serviceIdentity.subject} is not readable`)
129
+ const folder = await kernel.get(`${service.path.raw}/functions`)
130
+ const nodes = folder
131
+ ? await (
132
+ await kernel.children(folder.path, {
133
+ classes: [K.$.c('Function').path.class],
134
+ limit: 500,
135
+ })
136
+ ).all()
137
+ : []
138
+ identities = readFunctionIdentities(nodes, serviceIdentity.issuer)
139
+ cachedAt = Date.now()
140
+ }
141
+ const identity = identities.get(slug)
142
+ if (!identity) throw new Error(`Function identity function.${slug} is not registered`)
143
+ return hostedFunctionIdentity(identity, serviceIdentity)
144
+ }
145
+ }
146
+
147
+ /** Reuse only the Service's signing key. A hosted Function's inbound audience
148
+ * defaults to its own issuer; the Service HTTP origin is merely transport. */
149
+ export function hostedFunctionIdentity(
150
+ identity: { issuer: string; subject: string },
151
+ serviceIdentity: RemoteIdentityConfig,
152
+ ): RemoteIdentityConfig {
153
+ return { ...identity, privateKey: serviceIdentity.privateKey }
154
+ }
155
+
156
+ function readFunctionIdentities(
157
+ nodes: readonly Node[],
158
+ serviceIssuer: string,
159
+ ): Map<string, { issuer: string; subject: string }> {
160
+ const result = new Map<string, { issuer: string; subject: string }>()
161
+ for (const node of nodes) {
162
+ const ref = node.props[K.$.i('Function').ref.key]
163
+ const issuer = node.props[K.Identity.iss.key]
164
+ const subject = node.props[K.Identity.sub.key]
165
+ if (
166
+ typeof ref !== 'string' ||
167
+ !ref.startsWith('function.') ||
168
+ typeof issuer !== 'string' ||
169
+ typeof subject !== 'string'
170
+ ) {
171
+ continue
172
+ }
173
+ if (issuer !== serviceIssuer) {
174
+ throw new Error(`Function ${ref} does not share its hosting Service issuer`)
175
+ }
176
+ result.set(ref.slice('function.'.length), { issuer, subject })
177
+ }
178
+ return result
179
+ }
180
+
181
+ function assertLocalBindings(url: string, bindings: Record<string, FunctionBinding>): void {
182
+ const origin = new URL(url).origin
183
+ for (const [slug, binding] of Object.entries(bindings)) {
184
+ if (!binding.remoteUrl || new URL(binding.remoteUrl).origin !== origin) {
185
+ throw new Error(`serviceWorkerEntry function.${slug} must be hosted by this Service origin`)
186
+ }
187
+ }
188
+ }
189
+
190
+ function required<TEnv extends ServiceWorkerIdentityEnv>(env: TEnv, key: keyof TEnv): string {
191
+ const value = env[key]
192
+ if (typeof value !== 'string' || value.length === 0) {
193
+ throw new Error(`Missing service variable ${String(key)}`)
194
+ }
195
+ return value
196
+ }
197
+
198
+ function parsePrivateKey(value: string): JsonWebKey {
199
+ let parsed: unknown
200
+ try {
201
+ parsed = JSON.parse(value)
202
+ } catch (error) {
203
+ throw new Error('IDENTITY_PRIVATE_KEY is not valid JSON', { cause: error })
204
+ }
205
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
206
+ throw new Error('IDENTITY_PRIVATE_KEY is not a JWK object')
207
+ }
208
+ return parsed as JsonWebKey
209
+ }
@@ -6,6 +6,7 @@
6
6
  * Cloudflare, consumers go through `server.app.fetch` directly.
7
7
  */
8
8
 
9
+ import type { serve as nodeServe } from '@hono/node-server'
9
10
  import type { Hono } from 'hono'
10
11
 
11
12
  import type { RemoteServerHandle } from './handle.js'
@@ -15,7 +16,7 @@ export async function startNodeServer(app: Hono, port = 3000): Promise<RemoteSer
15
16
  // bundles (wrangler/esbuild) traverse it even though they never call it. A
16
17
  // literal import() would make them resolve the optional peer and fail.
17
18
  const nodeServerModule = '@hono/node-server'
18
- const { serve } = (await import(nodeServerModule)) as typeof import('@hono/node-server')
19
+ const { serve } = (await import(nodeServerModule)) as { serve: typeof nodeServe }
19
20
  // oxlint-disable-next-line no-explicit-any
20
21
  const server = serve({ fetch: app.fetch, port }) as any
21
22
 
@@ -30,8 +30,8 @@ import { createRemoteServer } from './create.js'
30
30
  import { requireEnv } from './require-env.js'
31
31
  import { canonicalizeServingUrl } from './serving-url.js'
32
32
 
33
- type Fetcher = { fetch(request: Request): Response | Promise<Response> }
34
- type App = {
33
+ export type Fetcher = { fetch(request: Request): Response | Promise<Response> }
34
+ export type WorkerApp = {
35
35
  fetch(
36
36
  request: Request,
37
37
  env?: unknown,
@@ -41,7 +41,7 @@ type App = {
41
41
 
42
42
  /** A built app cached by its serving URL: `origin` for same-origin matching,
43
43
  * `app` for in-process self-dispatch. */
44
- type CachedApp = { origin: string; app: App }
44
+ type CachedApp = { origin: string; app: WorkerApp }
45
45
 
46
46
  /**
47
47
  * Choose where an OUTBOUND subrequest to `u` is routed — or `null` to let the
@@ -137,6 +137,10 @@ export interface WorkerEntryConfig<TDeps> {
137
137
  rewriteRequest?: (env: TDeps, request: Request) => Request
138
138
  }
139
139
 
140
+ export interface AppWorkerEntryConfig<TDeps> extends Omit<WorkerEntryConfig<TDeps>, 'build'> {
141
+ buildApp: (url: string, env: TDeps) => WorkerApp
142
+ }
143
+
140
144
  export interface WorkerEntry<TDeps> {
141
145
  fetch(request: Request, env: TDeps, executionCtx?: ExecutionContext): Response | Promise<Response>
142
146
  }
@@ -201,6 +205,15 @@ export function assets<TDeps>(opts: {
201
205
  }
202
206
 
203
207
  export function createWorkerEntry<TDeps>(config: WorkerEntryConfig<TDeps>): WorkerEntry<TDeps> {
208
+ return createAppWorkerEntry({
209
+ ...config,
210
+ buildApp: (url, env) => createRemoteServer<TDeps>(config.build(url, env)).app,
211
+ })
212
+ }
213
+
214
+ export function createAppWorkerEntry<TDeps>(
215
+ config: AppWorkerEntryConfig<TDeps>,
216
+ ): WorkerEntry<TDeps> {
204
217
  // Cache the built app per distinct resolved URL — plural and bounded. On the
205
218
  // request-origin fallback the URL legitimately alternates for one worker
206
219
  // (direct http hits vs https-upgraded tunnel hits, workers.dev + custom
@@ -212,10 +225,10 @@ export function createWorkerEntry<TDeps>(config: WorkerEntryConfig<TDeps>): Work
212
225
  let self: Fetcher | null = null
213
226
  let routeEnv: TDeps | null = null
214
227
 
215
- function getApp(url: string, env: TDeps): App {
228
+ function getApp(url: string, env: TDeps): WorkerApp {
216
229
  const cached = apps.get(url)
217
230
  if (cached) return cached.app
218
- const { app } = createRemoteServer<TDeps>(config.build(url, env))
231
+ const app = config.buildApp(url, env)
219
232
  if (apps.size >= MAX_CACHED_APPS) {
220
233
  const oldest = apps.keys().next().value
221
234
  if (oldest !== undefined) apps.delete(oldest)
@@ -0,0 +1,164 @@
1
+ import type { FunctionBinding } from '@astrale-os/kernel-api/routed'
2
+ import type { FunctionSchema } from '@astrale-os/kernel-core/domain'
3
+
4
+ import { deriveAllowedAlgorithms } from '@astrale-os/kernel-core'
5
+ import { functionBindingSchema, functionOutputSchema } from '@astrale-os/kernel-core/domain'
6
+ import { importJWK, jwtVerify } from 'jose'
7
+ import { z } from 'zod'
8
+
9
+ import type { RemoteIdentityConfig } from '../auth/identity.js'
10
+
11
+ import { signCredential } from '../auth/sign.js'
12
+
13
+ export const SERVICE_FUNCTIONS_PATH = '/_astrale/functions'
14
+ export const SERVICE_FUNCTIONS_PURPOSE = 'astrale.service-functions.v1'
15
+
16
+ const JsonSchema = z.record(z.string(), z.unknown())
17
+
18
+ export const ServiceFunctionManifestEntrySchema = z.object({
19
+ slug: z.string().regex(/^[a-z][a-z0-9-]*$/),
20
+ ref: z.string().regex(/^function\.[a-z][a-z0-9-]*$/),
21
+ inputSchema: JsonSchema,
22
+ outputSchema: JsonSchema,
23
+ output: functionOutputSchema.default('value'),
24
+ binding: functionBindingSchema.unwrap(),
25
+ })
26
+
27
+ export const ServiceFunctionManifestSchema = z.object({
28
+ version: z.literal(1),
29
+ service: z.object({ issuer: z.string().min(1), subject: z.string().min(1) }),
30
+ functions: z.array(ServiceFunctionManifestEntrySchema).max(500),
31
+ })
32
+
33
+ export const ServiceFunctionDiscoveryRequestSchema = z.object({
34
+ kernelIssuer: z.string().url(),
35
+ nonce: z.string().min(16).max(256),
36
+ })
37
+
38
+ export const ServiceFunctionDiscoveryResponseSchema = ServiceFunctionManifestSchema.extend({
39
+ identity: z.object({ credential: z.string().min(1) }),
40
+ })
41
+
42
+ export type ServiceFunctionManifestEntry = z.infer<typeof ServiceFunctionManifestEntrySchema>
43
+ export type ServiceFunctionManifest = z.infer<typeof ServiceFunctionManifestSchema>
44
+ export type ServiceFunctionDiscoveryRequest = z.infer<typeof ServiceFunctionDiscoveryRequestSchema>
45
+ export type ServiceFunctionDiscoveryResponse = z.infer<
46
+ typeof ServiceFunctionDiscoveryResponseSchema
47
+ >
48
+
49
+ export function toServiceFunctionManifestEntries(
50
+ schemas: readonly FunctionSchema[],
51
+ ): ServiceFunctionManifestEntry[] {
52
+ return schemas
53
+ .map((schema) => {
54
+ const slug = schema.ref.slice('function.'.length)
55
+ return ServiceFunctionManifestEntrySchema.parse({
56
+ slug,
57
+ ref: schema.ref,
58
+ inputSchema: schema.inputSchema,
59
+ outputSchema: schema.outputSchema,
60
+ output: schema.output ?? 'value',
61
+ binding: schema.binding,
62
+ })
63
+ })
64
+ .sort((a, b) => a.slug.localeCompare(b.slug))
65
+ }
66
+
67
+ export async function signServiceFunctionManifest(
68
+ manifest: ServiceFunctionManifest,
69
+ request: ServiceFunctionDiscoveryRequest,
70
+ identity: RemoteIdentityConfig,
71
+ ): Promise<ServiceFunctionDiscoveryResponse> {
72
+ const parsedManifest = ServiceFunctionManifestSchema.parse(manifest)
73
+ const manifestHash = await hashServiceFunctionManifest(parsedManifest)
74
+ const credential = await signCredential(
75
+ { purpose: SERVICE_FUNCTIONS_PURPOSE, nonce: request.nonce, manifestHash },
76
+ {
77
+ issuer: identity.issuer,
78
+ subject: identity.subject,
79
+ audience: request.kernelIssuer,
80
+ privateKey: identity.privateKey,
81
+ ttl: '2m',
82
+ },
83
+ )
84
+ return { ...parsedManifest, identity: { credential } }
85
+ }
86
+
87
+ export async function verifyServiceFunctionManifest(
88
+ input: unknown,
89
+ expected: {
90
+ kernelIssuer: string
91
+ nonce: string
92
+ serviceIssuer: string
93
+ serviceSubject: string
94
+ publicKey: JsonWebKey
95
+ },
96
+ ): Promise<ServiceFunctionDiscoveryResponse> {
97
+ const response = ServiceFunctionDiscoveryResponseSchema.parse(input)
98
+ if (
99
+ response.service.issuer !== expected.serviceIssuer ||
100
+ response.service.subject !== expected.serviceSubject
101
+ ) {
102
+ throw new Error('service function manifest identity does not match the deployed Service')
103
+ }
104
+
105
+ const algorithms = deriveAllowedAlgorithms(expected.publicKey)
106
+ const algorithm = algorithms[0]
107
+ if (!algorithm) throw new Error('service function manifest public key has no supported algorithm')
108
+ const key = await importJWK(expected.publicKey, algorithm)
109
+ const verified = await jwtVerify(response.identity.credential, key, {
110
+ algorithms,
111
+ issuer: expected.serviceIssuer,
112
+ subject: expected.serviceSubject,
113
+ audience: expected.kernelIssuer,
114
+ })
115
+ const expectedHash = await hashServiceFunctionManifest({
116
+ version: response.version,
117
+ service: response.service,
118
+ functions: response.functions,
119
+ })
120
+ if (verified.payload.purpose !== SERVICE_FUNCTIONS_PURPOSE) {
121
+ throw new Error('service function manifest credential has the wrong purpose')
122
+ }
123
+ if (verified.payload.nonce !== expected.nonce) {
124
+ throw new Error('service function manifest credential has the wrong nonce')
125
+ }
126
+ if (verified.payload.manifestHash !== expectedHash) {
127
+ throw new Error('service function manifest credential does not cover the returned manifest')
128
+ }
129
+ return response
130
+ }
131
+
132
+ export async function hashServiceFunctionManifest(
133
+ manifest: ServiceFunctionManifest,
134
+ ): Promise<string> {
135
+ const bytes = new TextEncoder().encode(
136
+ canonicalJson(ServiceFunctionManifestSchema.parse(manifest)),
137
+ )
138
+ const digest = await crypto.subtle.digest('SHA-256', bytes)
139
+ return `sha256:${hex(new Uint8Array(digest))}`
140
+ }
141
+
142
+ export function functionBindingForManifest(
143
+ binding: FunctionBinding,
144
+ auth: 'required' | 'optional' | 'public' | undefined,
145
+ ): FunctionBinding {
146
+ return { ...binding, auth: auth ?? 'required' }
147
+ }
148
+
149
+ function canonicalJson(value: unknown): string {
150
+ if (value === null || typeof value !== 'object') return JSON.stringify(value)
151
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`
152
+ const entries = Object.entries(value as Record<string, unknown>)
153
+ .filter(([, item]) => item !== undefined)
154
+ .sort(([a], [b]) => a.localeCompare(b))
155
+ return `{${entries
156
+ .map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`)
157
+ .join(',')}}`
158
+ }
159
+
160
+ function hex(bytes: Uint8Array): string {
161
+ let value = ''
162
+ for (const byte of bytes) value += byte.toString(16).padStart(2, '0')
163
+ return value
164
+ }
@@ -0,0 +1,18 @@
1
+ export {
2
+ SERVICE_FUNCTIONS_PATH,
3
+ SERVICE_FUNCTIONS_PURPOSE,
4
+ ServiceFunctionDiscoveryRequestSchema,
5
+ ServiceFunctionDiscoveryResponseSchema,
6
+ ServiceFunctionManifestEntrySchema,
7
+ ServiceFunctionManifestSchema,
8
+ hashServiceFunctionManifest,
9
+ signServiceFunctionManifest,
10
+ toServiceFunctionManifestEntries,
11
+ verifyServiceFunctionManifest,
12
+ } from './functions.js'
13
+ export type {
14
+ ServiceFunctionDiscoveryRequest,
15
+ ServiceFunctionDiscoveryResponse,
16
+ ServiceFunctionManifest,
17
+ ServiceFunctionManifestEntry,
18
+ } from './functions.js'
@@ -1,24 +0,0 @@
1
- import type { Context } from 'hono';
2
- /** Internal structural host capability; not part of the author-facing SDK contract. */
3
- export type HandlerExecutionLifetime = {
4
- waitUntil(promise: Promise<unknown>): void;
5
- };
6
- /**
7
- * Work detached from the current handler result.
8
- *
9
- * The callback is deliberately lazy: the SDK registers the resulting task with
10
- * the active execution backend before it starts. Current request-based hosts
11
- * extend their request lifetime; future durable backends may provide stronger
12
- * execution guarantees without changing the author-facing API.
13
- */
14
- export type DeferredTask = () => void | PromiseLike<unknown>;
15
- export type Defer = (task: DeferredTask) => void;
16
- export type HandlerLifecycle = {
17
- defer: Defer;
18
- settle(): Promise<void>;
19
- };
20
- /** Bind detached work to the host, with a safe blocking fallback for other runtimes. */
21
- export declare function createHandlerLifecycle(lifetime?: HandlerExecutionLifetime): HandlerLifecycle;
22
- /** Hono omits its execution context in Node and in lightweight unit tests. */
23
- export declare function executionLifetime(c: Context): HandlerExecutionLifetime | undefined;
24
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/defer/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,uFAAuF;AACvF,MAAM,MAAM,wBAAwB,GAAG;IACrC,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAA;CAC3C,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,CAAA;AAE5D,MAAM,MAAM,KAAK,GAAG,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAA;AAEhD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,KAAK,CAAA;IACZ,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACxB,CAAA;AAED,wFAAwF;AACxF,wBAAgB,sBAAsB,CAAC,QAAQ,CAAC,EAAE,wBAAwB,GAAG,gBAAgB,CA4B5F;AAED,8EAA8E;AAC9E,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,OAAO,GAAG,wBAAwB,GAAG,SAAS,CAOlF"}
@@ -1,42 +0,0 @@
1
- /** Bind detached work to the host, with a safe blocking fallback for other runtimes. */
2
- export function createHandlerLifecycle(lifetime) {
3
- const pending = [];
4
- return {
5
- defer(task) {
6
- const promise = Promise.resolve().then(task);
7
- if (lifetime)
8
- lifetime.waitUntil(promise);
9
- else
10
- pending.push(promise);
11
- },
12
- async settle() {
13
- if (lifetime || pending.length === 0)
14
- return;
15
- // A deferred task may register more work before it settles. Drain until
16
- // the lifecycle is stable so non-request runtimes never abandon children.
17
- let cursor = 0;
18
- while (cursor < pending.length) {
19
- const batch = pending.slice(cursor);
20
- cursor = pending.length;
21
- const results = await Promise.allSettled(batch);
22
- for (const result of results) {
23
- if (result.status === 'rejected') {
24
- // oxlint-disable-next-line no-console
25
- console.error('[sdk] deferred task failed:', result.reason);
26
- }
27
- }
28
- }
29
- },
30
- };
31
- }
32
- /** Hono omits its execution context in Node and in lightweight unit tests. */
33
- export function executionLifetime(c) {
34
- try {
35
- const host = c.executionCtx;
36
- return { waitUntil: (promise) => host.waitUntil(promise) };
37
- }
38
- catch {
39
- return undefined;
40
- }
41
- }
42
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/defer/index.ts"],"names":[],"mappings":"AAwBA,wFAAwF;AACxF,MAAM,UAAU,sBAAsB,CAAC,QAAmC;IACxE,MAAM,OAAO,GAAuB,EAAE,CAAA;IAEtC,OAAO;QACL,KAAK,CAAC,IAAI;YACR,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YAC5C,IAAI,QAAQ;gBAAE,QAAQ,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;;gBACpC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QAC5B,CAAC;QACD,KAAK,CAAC,MAAM;YACV,IAAI,QAAQ,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAM;YAE5C,wEAAwE;YACxE,0EAA0E;YAC1E,IAAI,MAAM,GAAG,CAAC,CAAA;YACd,OAAO,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;gBAC/B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;gBACnC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAA;gBACvB,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,CAAA;gBAC/C,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;oBAC7B,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;wBACjC,sCAAsC;wBACtC,OAAO,CAAC,KAAK,CAAC,6BAA6B,EAAE,MAAM,CAAC,MAAM,CAAC,CAAA;oBAC7D,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;KACF,CAAA;AACH,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,iBAAiB,CAAC,CAAU;IAC1C,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,CAAC,CAAC,YAAY,CAAA;QAC3B,OAAO,EAAE,SAAS,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAA;IAC5D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC"}
@@ -1,64 +0,0 @@
1
- import type { Context } from 'hono'
2
-
3
- /** Internal structural host capability; not part of the author-facing SDK contract. */
4
- export type HandlerExecutionLifetime = {
5
- waitUntil(promise: Promise<unknown>): void
6
- }
7
-
8
- /**
9
- * Work detached from the current handler result.
10
- *
11
- * The callback is deliberately lazy: the SDK registers the resulting task with
12
- * the active execution backend before it starts. Current request-based hosts
13
- * extend their request lifetime; future durable backends may provide stronger
14
- * execution guarantees without changing the author-facing API.
15
- */
16
- export type DeferredTask = () => void | PromiseLike<unknown>
17
-
18
- export type Defer = (task: DeferredTask) => void
19
-
20
- export type HandlerLifecycle = {
21
- defer: Defer
22
- settle(): Promise<void>
23
- }
24
-
25
- /** Bind detached work to the host, with a safe blocking fallback for other runtimes. */
26
- export function createHandlerLifecycle(lifetime?: HandlerExecutionLifetime): HandlerLifecycle {
27
- const pending: Promise<unknown>[] = []
28
-
29
- return {
30
- defer(task) {
31
- const promise = Promise.resolve().then(task)
32
- if (lifetime) lifetime.waitUntil(promise)
33
- else pending.push(promise)
34
- },
35
- async settle() {
36
- if (lifetime || pending.length === 0) return
37
-
38
- // A deferred task may register more work before it settles. Drain until
39
- // the lifecycle is stable so non-request runtimes never abandon children.
40
- let cursor = 0
41
- while (cursor < pending.length) {
42
- const batch = pending.slice(cursor)
43
- cursor = pending.length
44
- const results = await Promise.allSettled(batch)
45
- for (const result of results) {
46
- if (result.status === 'rejected') {
47
- // oxlint-disable-next-line no-console
48
- console.error('[sdk] deferred task failed:', result.reason)
49
- }
50
- }
51
- }
52
- },
53
- }
54
- }
55
-
56
- /** Hono omits its execution context in Node and in lightweight unit tests. */
57
- export function executionLifetime(c: Context): HandlerExecutionLifetime | undefined {
58
- try {
59
- const host = c.executionCtx
60
- return { waitUntil: (promise) => host.waitUntil(promise) }
61
- } catch {
62
- return undefined
63
- }
64
- }