@astrale-os/kernel-server 0.5.0-beta.8 → 1.0.0-beta.1

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 (241) hide show
  1. package/dist/capture.d.ts +0 -1
  2. package/dist/capture.js +0 -1
  3. package/dist/cors/cors.d.ts +0 -1
  4. package/dist/cors/cors.js +11 -4
  5. package/dist/cors/index.d.ts +0 -1
  6. package/dist/cors/index.js +0 -1
  7. package/dist/cors/types.d.ts +0 -1
  8. package/dist/cors/types.js +0 -1
  9. package/dist/errors/admission.d.ts +7 -0
  10. package/dist/errors/admission.js +22 -0
  11. package/dist/errors/availability.d.ts +0 -1
  12. package/dist/errors/availability.js +0 -1
  13. package/dist/errors/classify.d.ts +8 -1
  14. package/dist/errors/classify.js +15 -1
  15. package/dist/errors/index.d.ts +2 -2
  16. package/dist/errors/index.js +2 -2
  17. package/dist/errors/request-body.d.ts +0 -1
  18. package/dist/errors/request-body.js +0 -1
  19. package/dist/errors/response-body.d.ts +0 -1
  20. package/dist/errors/response-body.js +0 -1
  21. package/dist/index.d.ts +3 -4
  22. package/dist/index.js +2 -3
  23. package/dist/invocation/context.d.ts +3 -3
  24. package/dist/invocation/context.js +2 -2
  25. package/dist/invocation/identity.d.ts +1 -2
  26. package/dist/invocation/identity.js +1 -2
  27. package/dist/invocation/index.d.ts +1 -2
  28. package/dist/invocation/index.js +1 -2
  29. package/dist/invocation/release-route.d.ts +22 -0
  30. package/dist/invocation/release-route.js +32 -0
  31. package/dist/invocation/types.d.ts +8 -7
  32. package/dist/invocation/types.js +0 -1
  33. package/dist/issuer/exchange.d.ts +0 -1
  34. package/dist/issuer/exchange.js +20 -10
  35. package/dist/issuer/index.d.ts +0 -1
  36. package/dist/issuer/index.js +0 -1
  37. package/dist/issuer/routes.d.ts +0 -1
  38. package/dist/issuer/routes.js +57 -48
  39. package/dist/issuer/types.d.ts +5 -1
  40. package/dist/issuer/types.js +0 -1
  41. package/dist/lifecycle/active.d.ts +4 -1
  42. package/dist/lifecycle/active.js +48 -11
  43. package/dist/lifecycle/index.d.ts +0 -1
  44. package/dist/lifecycle/index.js +0 -1
  45. package/dist/release/delivery/handler.d.ts +9 -0
  46. package/dist/{publication → release}/delivery/handler.js +19 -13
  47. package/dist/release/delivery/index.d.ts +1 -0
  48. package/dist/release/delivery/index.js +1 -0
  49. package/dist/{publication → release}/delivery/types.d.ts +4 -5
  50. package/dist/release/delivery/types.js +1 -0
  51. package/dist/release/index.d.ts +3 -0
  52. package/dist/release/index.js +2 -0
  53. package/dist/release/served.d.ts +21 -0
  54. package/dist/release/served.js +10 -0
  55. package/dist/release/snapshot.d.ts +22 -0
  56. package/dist/release/snapshot.js +49 -0
  57. package/dist/release/table.d.ts +4 -0
  58. package/dist/release/table.js +11 -0
  59. package/dist/release/types.d.ts +23 -0
  60. package/dist/release/types.js +1 -0
  61. package/dist/routes/address.d.ts +2 -0
  62. package/dist/routes/address.js +6 -0
  63. package/dist/routes/binary.d.ts +9 -0
  64. package/dist/routes/binary.js +110 -0
  65. package/dist/routes/bundle.d.ts +5 -0
  66. package/dist/routes/bundle.js +50 -0
  67. package/dist/routes/csrf.d.ts +16 -0
  68. package/dist/routes/csrf.js +35 -0
  69. package/dist/routes/document.d.ts +6 -0
  70. package/dist/routes/document.js +57 -0
  71. package/dist/routes/index.d.ts +3 -2
  72. package/dist/routes/index.js +2 -2
  73. package/dist/routes/model.d.ts +15 -0
  74. package/dist/routes/model.js +1 -0
  75. package/dist/routes/native.d.ts +17 -0
  76. package/dist/routes/native.js +126 -0
  77. package/dist/routes/preflight.d.ts +4 -0
  78. package/dist/routes/preflight.js +55 -0
  79. package/dist/routes/response.d.ts +14 -0
  80. package/dist/routes/response.js +33 -0
  81. package/dist/routes/routes.d.ts +7 -24
  82. package/dist/routes/routes.js +24 -439
  83. package/dist/routes/snapshot.d.ts +13 -0
  84. package/dist/routes/snapshot.js +39 -0
  85. package/dist/routes/stream.d.ts +6 -0
  86. package/dist/routes/stream.js +122 -0
  87. package/dist/server.d.ts +0 -1
  88. package/dist/server.js +95 -51
  89. package/dist/transport/body.d.ts +0 -1
  90. package/dist/transport/body.js +0 -1
  91. package/dist/transport/codecs.d.ts +2 -3
  92. package/dist/transport/codecs.js +4 -4
  93. package/dist/transport/credential.d.ts +0 -1
  94. package/dist/transport/credential.js +0 -1
  95. package/dist/transport/http/binary-request.d.ts +11 -0
  96. package/dist/transport/http/binary-request.js +102 -0
  97. package/dist/transport/http/binary.d.ts +6 -0
  98. package/dist/transport/http/binary.js +35 -0
  99. package/dist/transport/http/byte-stream.d.ts +4 -0
  100. package/dist/transport/http/byte-stream.js +123 -0
  101. package/dist/transport/http/control.d.ts +12 -0
  102. package/dist/transport/http/control.js +24 -0
  103. package/dist/transport/http/handler.d.ts +8 -3
  104. package/dist/transport/http/handler.js +97 -45
  105. package/dist/transport/http/index.d.ts +3 -2
  106. package/dist/transport/http/index.js +2 -2
  107. package/dist/transport/http/model.d.ts +20 -0
  108. package/dist/transport/http/model.js +1 -0
  109. package/dist/transport/http/render.d.ts +3 -31
  110. package/dist/transport/http/render.js +18 -269
  111. package/dist/transport/http/response.d.ts +14 -0
  112. package/dist/transport/http/response.js +77 -0
  113. package/dist/transport/http/stream.d.ts +5 -0
  114. package/dist/transport/http/stream.js +134 -0
  115. package/dist/transport/index.d.ts +2 -2
  116. package/dist/transport/index.js +2 -2
  117. package/dist/transport/result.d.ts +13 -0
  118. package/dist/transport/result.js +35 -0
  119. package/dist/transport/router.d.ts +0 -1
  120. package/dist/transport/router.js +0 -1
  121. package/dist/transport/stream-error.d.ts +0 -1
  122. package/dist/transport/stream-error.js +0 -1
  123. package/dist/transport/types.d.ts +1 -2
  124. package/dist/transport/types.js +0 -1
  125. package/dist/transport/websocket/index.d.ts +0 -1
  126. package/dist/transport/websocket/index.js +0 -1
  127. package/dist/transport/websocket/session.d.ts +3 -3
  128. package/dist/transport/websocket/session.js +30 -30
  129. package/dist/transport/websocket/upgrade.d.ts +2 -3
  130. package/dist/transport/websocket/upgrade.js +7 -7
  131. package/dist/types.d.ts +33 -10
  132. package/dist/types.js +0 -1
  133. package/package.json +11 -22
  134. package/dist/capture.d.ts.map +0 -1
  135. package/dist/capture.js.map +0 -1
  136. package/dist/cors/cors.d.ts.map +0 -1
  137. package/dist/cors/cors.js.map +0 -1
  138. package/dist/cors/index.d.ts.map +0 -1
  139. package/dist/cors/index.js.map +0 -1
  140. package/dist/cors/types.d.ts.map +0 -1
  141. package/dist/cors/types.js.map +0 -1
  142. package/dist/errors/availability.d.ts.map +0 -1
  143. package/dist/errors/availability.js.map +0 -1
  144. package/dist/errors/classify.d.ts.map +0 -1
  145. package/dist/errors/classify.js.map +0 -1
  146. package/dist/errors/index.d.ts.map +0 -1
  147. package/dist/errors/index.js.map +0 -1
  148. package/dist/errors/request-body.d.ts.map +0 -1
  149. package/dist/errors/request-body.js.map +0 -1
  150. package/dist/errors/response-body.d.ts.map +0 -1
  151. package/dist/errors/response-body.js.map +0 -1
  152. package/dist/index.d.ts.map +0 -1
  153. package/dist/index.js.map +0 -1
  154. package/dist/invocation/context.d.ts.map +0 -1
  155. package/dist/invocation/context.js.map +0 -1
  156. package/dist/invocation/identity.d.ts.map +0 -1
  157. package/dist/invocation/identity.js.map +0 -1
  158. package/dist/invocation/index.d.ts.map +0 -1
  159. package/dist/invocation/index.js.map +0 -1
  160. package/dist/invocation/publication-route.d.ts +0 -14
  161. package/dist/invocation/publication-route.d.ts.map +0 -1
  162. package/dist/invocation/publication-route.js +0 -45
  163. package/dist/invocation/publication-route.js.map +0 -1
  164. package/dist/invocation/types.d.ts.map +0 -1
  165. package/dist/invocation/types.js.map +0 -1
  166. package/dist/issuer/exchange.d.ts.map +0 -1
  167. package/dist/issuer/exchange.js.map +0 -1
  168. package/dist/issuer/index.d.ts.map +0 -1
  169. package/dist/issuer/index.js.map +0 -1
  170. package/dist/issuer/routes.d.ts.map +0 -1
  171. package/dist/issuer/routes.js.map +0 -1
  172. package/dist/issuer/types.d.ts.map +0 -1
  173. package/dist/issuer/types.js.map +0 -1
  174. package/dist/lifecycle/active.d.ts.map +0 -1
  175. package/dist/lifecycle/active.js.map +0 -1
  176. package/dist/lifecycle/index.d.ts.map +0 -1
  177. package/dist/lifecycle/index.js.map +0 -1
  178. package/dist/publication/delivery/handler.d.ts +0 -10
  179. package/dist/publication/delivery/handler.d.ts.map +0 -1
  180. package/dist/publication/delivery/handler.js.map +0 -1
  181. package/dist/publication/delivery/index.d.ts +0 -3
  182. package/dist/publication/delivery/index.d.ts.map +0 -1
  183. package/dist/publication/delivery/index.js +0 -2
  184. package/dist/publication/delivery/index.js.map +0 -1
  185. package/dist/publication/delivery/types.d.ts.map +0 -1
  186. package/dist/publication/delivery/types.js +0 -2
  187. package/dist/publication/delivery/types.js.map +0 -1
  188. package/dist/publication/index.d.ts +0 -4
  189. package/dist/publication/index.d.ts.map +0 -1
  190. package/dist/publication/index.js +0 -3
  191. package/dist/publication/index.js.map +0 -1
  192. package/dist/publication/snapshot.d.ts +0 -16
  193. package/dist/publication/snapshot.d.ts.map +0 -1
  194. package/dist/publication/snapshot.js +0 -39
  195. package/dist/publication/snapshot.js.map +0 -1
  196. package/dist/publication/table.d.ts +0 -4
  197. package/dist/publication/table.d.ts.map +0 -1
  198. package/dist/publication/table.js +0 -17
  199. package/dist/publication/table.js.map +0 -1
  200. package/dist/publication/types.d.ts +0 -23
  201. package/dist/publication/types.d.ts.map +0 -1
  202. package/dist/publication/types.js +0 -2
  203. package/dist/publication/types.js.map +0 -1
  204. package/dist/publication/validator.d.ts +0 -4
  205. package/dist/publication/validator.d.ts.map +0 -1
  206. package/dist/publication/validator.js +0 -17
  207. package/dist/publication/validator.js.map +0 -1
  208. package/dist/routes/index.d.ts.map +0 -1
  209. package/dist/routes/index.js.map +0 -1
  210. package/dist/routes/routes.d.ts.map +0 -1
  211. package/dist/routes/routes.js.map +0 -1
  212. package/dist/server.d.ts.map +0 -1
  213. package/dist/server.js.map +0 -1
  214. package/dist/transport/body.d.ts.map +0 -1
  215. package/dist/transport/body.js.map +0 -1
  216. package/dist/transport/codecs.d.ts.map +0 -1
  217. package/dist/transport/codecs.js.map +0 -1
  218. package/dist/transport/credential.d.ts.map +0 -1
  219. package/dist/transport/credential.js.map +0 -1
  220. package/dist/transport/http/handler.d.ts.map +0 -1
  221. package/dist/transport/http/handler.js.map +0 -1
  222. package/dist/transport/http/index.d.ts.map +0 -1
  223. package/dist/transport/http/index.js.map +0 -1
  224. package/dist/transport/http/render.d.ts.map +0 -1
  225. package/dist/transport/http/render.js.map +0 -1
  226. package/dist/transport/index.d.ts.map +0 -1
  227. package/dist/transport/index.js.map +0 -1
  228. package/dist/transport/router.d.ts.map +0 -1
  229. package/dist/transport/router.js.map +0 -1
  230. package/dist/transport/stream-error.d.ts.map +0 -1
  231. package/dist/transport/stream-error.js.map +0 -1
  232. package/dist/transport/types.d.ts.map +0 -1
  233. package/dist/transport/types.js.map +0 -1
  234. package/dist/transport/websocket/index.d.ts.map +0 -1
  235. package/dist/transport/websocket/index.js.map +0 -1
  236. package/dist/transport/websocket/session.d.ts.map +0 -1
  237. package/dist/transport/websocket/session.js.map +0 -1
  238. package/dist/transport/websocket/upgrade.d.ts.map +0 -1
  239. package/dist/transport/websocket/upgrade.js.map +0 -1
  240. package/dist/types.d.ts.map +0 -1
  241. package/dist/types.js.map +0 -1
@@ -2,7 +2,8 @@ import { configuration } from '@astrale-os/kernel-protocol/issuer/configuration'
2
2
  import { hosted } from '@astrale-os/kernel-protocol/issuer/hosted';
3
3
  import { keySet } from '@astrale-os/kernel-protocol/issuer/key-set';
4
4
  import { paths as issuerPaths } from '@astrale-os/kernel-protocol/issuer/paths';
5
- import { IssuerKeysUnavailableError } from '../errors/index.js';
5
+ import { workflowEngine } from '@astrale-os/kernel-protocol/issuer/workflow-engine';
6
+ import { admissionRefusal, IssuerKeysUnavailableError, retryAfterHeaders } from '../errors/index.js';
6
7
  import { mount } from './exchange.js';
7
8
  export function issuerRouteReservations(input, tokenExchange = false) {
8
9
  const paths = issuerPaths(input.issuer);
@@ -13,6 +14,9 @@ export function issuerRouteReservations(input, tokenExchange = false) {
13
14
  if (tokenExchange) {
14
15
  reservations.push(Object.freeze({ path: paths.tokenExchange, methods: Object.freeze(['POST']) }));
15
16
  }
17
+ if (input.workflowEngineKeys !== undefined) {
18
+ reservations.push(Object.freeze({ path: paths.workflowEngine, methods: Object.freeze(['GET']) }));
19
+ }
16
20
  if (input.loadIssuerPublicKeys !== undefined) {
17
21
  const base = new URL(input.issuer).pathname.replace(/\/+$/u, '');
18
22
  reservations.push(Object.freeze({
@@ -32,11 +36,28 @@ export function mountIssuer(app, input, cors, active, timeoutMs, tokenExchange)
32
36
  ...cors.response(request),
33
37
  'cache-control': 'no-cache',
34
38
  });
39
+ // A lifecycle refusal names saturation or drain and presents its retry delay.
40
+ const refusal = (request) => {
41
+ const refused = admissionRefusal(active.isAccepting);
42
+ const retry = retryAfterHeaders(refused);
43
+ return {
44
+ body: { error: refused.message },
45
+ headers: {
46
+ ...cors.response(request, Object.keys(retry)),
47
+ 'cache-control': 'no-cache',
48
+ ...retry,
49
+ },
50
+ };
51
+ };
35
52
  const algorithm = input.signingAlgorithm ?? 'ES256';
36
53
  const discovery = configuration(issuer, [algorithm], {
37
54
  tokenExchange: tokenExchange !== undefined,
38
55
  });
39
56
  app.get(paths.configuration, (context) => context.json(discovery, 200, headers(context.req.raw)));
57
+ if (input.workflowEngineKeys !== undefined) {
58
+ const engine = workflowEngine(issuer, input.workflowEngineKeys);
59
+ app.get(paths.workflowEngine, (context) => context.json(engine, 200, headers(context.req.raw)));
60
+ }
40
61
  if (cors.enabled) {
41
62
  app.options(paths.configuration, (context) => cors.preflight(context.req.raw, { methods: ['GET'] }));
42
63
  app.options(paths.keys, (context) => cors.preflight(context.req.raw, { methods: ['GET'] }));
@@ -44,7 +65,8 @@ export function mountIssuer(app, input, cors, active, timeoutMs, tokenExchange)
44
65
  app.get(paths.keys, async (context) => {
45
66
  const operation = active.begin(timeoutMs, context.req.raw.signal);
46
67
  if (operation === undefined) {
47
- return context.json({ error: 'Issuer is unavailable while draining.' }, 503, headers(context.req.raw));
68
+ const refused = refusal(context.req.raw);
69
+ return context.json(refused.body, 503, refused.headers);
48
70
  }
49
71
  try {
50
72
  const keys = await input.loadPublicKeys(operation.signal);
@@ -70,54 +92,37 @@ export function mountIssuer(app, input, cors, active, timeoutMs, tokenExchange)
70
92
  app.options(`${base}/iss/:thumbprint/.well-known/openid-configuration`, (context) => cors.preflight(context.req.raw, { methods: ['GET'] }));
71
93
  app.options(`${base}/iss/:thumbprint/.well-known/jwks.json`, (context) => cors.preflight(context.req.raw, { methods: ['GET'] }));
72
94
  }
73
- app.get(`${base}/iss/:thumbprint/.well-known/openid-configuration`, async (context) => {
74
- const hostedIssuer = hosted(issuer, context.req.param('thumbprint'));
75
- if (hostedIssuer === undefined)
76
- return context.json({ error: 'Issuer not found.' }, 404, headers(context.req.raw));
77
- const operation = active.begin(timeoutMs, context.req.raw.signal);
78
- if (operation === undefined) {
79
- return context.json({ error: 'Issuer is unavailable while draining.' }, 503, headers(context.req.raw));
80
- }
81
- try {
82
- const keys = await hostedKeys(loadIssuerPublicKeys, hostedIssuer, operation.signal, algorithm);
83
- if (keys.length === 0)
95
+ const mountHostedDiscovery = (path, document) => {
96
+ app.get(path, async (context) => {
97
+ const hostedIssuer = hosted(issuer, context.req.param('thumbprint'));
98
+ if (hostedIssuer === undefined)
84
99
  return context.json({ error: 'Issuer not found.' }, 404, headers(context.req.raw));
85
- return context.json(configuration(hostedIssuer, [algorithm]), 200, headers(context.req.raw));
86
- }
87
- catch (cause) {
88
- if (operation.signal.aborted || cause instanceof IssuerKeysUnavailableError) {
89
- return context.json({ error: 'Issuer keys are unavailable.' }, 503, headers(context.req.raw));
100
+ const operation = active.begin(timeoutMs, context.req.raw.signal);
101
+ if (operation === undefined) {
102
+ const refused = refusal(context.req.raw);
103
+ return context.json(refused.body, 503, refused.headers);
90
104
  }
91
- throw cause;
92
- }
93
- finally {
94
- operation.settle();
95
- }
96
- });
97
- app.get(`${base}/iss/:thumbprint/.well-known/jwks.json`, async (context) => {
98
- const hostedIssuer = hosted(issuer, context.req.param('thumbprint'));
99
- if (hostedIssuer === undefined)
100
- return context.json({ error: 'Issuer not found.' }, 404, headers(context.req.raw));
101
- const operation = active.begin(timeoutMs, context.req.raw.signal);
102
- if (operation === undefined) {
103
- return context.json({ error: 'Issuer is unavailable while draining.' }, 503, headers(context.req.raw));
104
- }
105
- try {
106
- const keys = await hostedKeys(loadIssuerPublicKeys, hostedIssuer, operation.signal, algorithm);
107
- if (keys.length === 0)
108
- return context.json({ error: 'Issuer not found.' }, 404, headers(context.req.raw));
109
- return context.json({ keys }, 200, headers(context.req.raw));
110
- }
111
- catch (cause) {
112
- if (operation.signal.aborted || cause instanceof IssuerKeysUnavailableError) {
113
- return context.json({ error: 'Issuer keys are unavailable.' }, 503, headers(context.req.raw));
105
+ try {
106
+ const keys = await hostedKeys(loadIssuerPublicKeys, hostedIssuer, operation.signal, algorithm);
107
+ if (keys.length === 0)
108
+ return context.json({ error: 'Issuer not found.' }, 404, headers(context.req.raw));
109
+ return context.json(document(hostedIssuer, keys), 200, headers(context.req.raw));
114
110
  }
115
- throw cause;
116
- }
117
- finally {
118
- operation.settle();
119
- }
120
- });
111
+ catch (cause) {
112
+ if (operation.signal.aborted || cause instanceof IssuerKeysUnavailableError) {
113
+ return context.json({ error: 'Issuer keys are unavailable.' }, 503, headers(context.req.raw));
114
+ }
115
+ throw cause;
116
+ }
117
+ finally {
118
+ operation.settle();
119
+ }
120
+ });
121
+ };
122
+ mountHostedDiscovery(`${base}/iss/:thumbprint/.well-known/openid-configuration`, (issuer) => configuration(issuer, [algorithm]));
123
+ mountHostedDiscovery(`${base}/iss/:thumbprint/.well-known/jwks.json`, (_issuer, keys) => ({
124
+ keys,
125
+ }));
121
126
  }
122
127
  }
123
128
  async function hostedKeys(load, issuer, signal, algorithm) {
@@ -136,9 +141,14 @@ export function admitIssuer(input) {
136
141
  }
137
142
  const loadPublicKeys = input.loadPublicKeys;
138
143
  const loadIssuerPublicKeys = input.loadIssuerPublicKeys;
144
+ // A Kernel that drives an engine publishes it; invalid keys fail closed at boot.
145
+ const workflowEngineKeys = input.workflowEngineKeys === undefined
146
+ ? undefined
147
+ : workflowEngine(issuer, input.workflowEngineKeys).identity_keys;
139
148
  return Object.freeze({
140
149
  issuer,
141
150
  signingAlgorithm,
151
+ ...(workflowEngineKeys === undefined ? {} : { workflowEngineKeys }),
142
152
  loadPublicKeys: (signal) => loadPublicKeys.call(input, signal),
143
153
  ...(loadIssuerPublicKeys === undefined
144
154
  ? {}
@@ -147,4 +157,3 @@ export function admitIssuer(input) {
147
157
  }),
148
158
  });
149
159
  }
150
- //# sourceMappingURL=routes.js.map
@@ -8,5 +8,9 @@ export interface Issuer {
8
8
  loadPublicKeys(signal: AbortSignal): Promise<readonly JsonWebKey[]>;
9
9
  /** Optional hosted-key discovery for issuers below `<issuer>/iss/:thumbprint`. */
10
10
  readonly loadIssuerPublicKeys?: (issuer: IssuerId, signal: AbortSignal) => Promise<readonly JsonWebKey[]>;
11
+ /**
12
+ * Request identity keys of the workflow engine this Kernel drives, published at
13
+ * `<issuer>/.well-known/astrale/workflow-engine.json` so Domains verify the engine's requests.
14
+ */
15
+ readonly workflowEngineKeys?: readonly string[];
11
16
  }
12
- //# sourceMappingURL=types.d.ts.map
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=types.js.map
@@ -12,6 +12,8 @@ export interface ActiveOperation {
12
12
  /** Register cooperative cleanup and return an idempotent unregistration function. */
13
13
  onCancel(cancel: CancelWork): () => void;
14
14
  cancel(reason: string): void;
15
+ /** Replace the remaining budget after a trusted transport binding is admitted. */
16
+ extend(timeoutMs: number): void;
15
17
  settle(): void;
16
18
  readonly finished: Promise<void>;
17
19
  }
@@ -26,6 +28,8 @@ export declare class ActiveOperations {
26
28
  private changed;
27
29
  constructor(maximumConcurrentOperations: number);
28
30
  get isAccepting(): boolean;
31
+ /** Whether a newly arriving operation can be admitted without semantic dispatch. */
32
+ get isAvailable(): boolean;
29
33
  begin(timeoutMs: number, externalSignal?: AbortSignal): ActiveOperation | undefined;
30
34
  attachSocket(socket: ActiveSocket): boolean;
31
35
  detachSocket(socket: ActiveSocket): void;
@@ -37,4 +41,3 @@ export declare class ActiveOperations {
37
41
  private closeSockets;
38
42
  }
39
43
  export {};
40
- //# sourceMappingURL=active.d.ts.map
@@ -1,3 +1,6 @@
1
+ // Streaming iterators can need several microtasks to return after cancellation. Keep the
2
+ // cleanup join bounded even when a producer ignores its cancellation signal.
3
+ const CANCELLATION_CLEANUP_GRACE_MS = 250;
1
4
  export class ActiveOperations {
2
5
  maximumConcurrentOperations;
3
6
  accepting = true;
@@ -13,10 +16,13 @@ export class ActiveOperations {
13
16
  get isAccepting() {
14
17
  return this.accepting;
15
18
  }
19
+ /** Whether a newly arriving operation can be admitted without semantic dispatch. */
20
+ get isAvailable() {
21
+ return this.accepting && this.operations.size < this.maximumConcurrentOperations;
22
+ }
16
23
  begin(timeoutMs, externalSignal) {
17
- if (!this.accepting || this.operations.size >= this.maximumConcurrentOperations) {
24
+ if (!this.isAvailable)
18
25
  return undefined;
19
- }
20
26
  const operation = new Operation(Date.now() + timeoutMs, () => {
21
27
  this.operations.delete(operation);
22
28
  this.changed?.();
@@ -63,19 +69,32 @@ export class ActiveOperations {
63
69
  }
64
70
  close(deadline) {
65
71
  this.drain();
66
- this.closing ??= this.closeOnce(deadline);
72
+ if (this.closing === undefined) {
73
+ const flight = this.closeOnce(deadline);
74
+ this.closing = flight;
75
+ void flight.catch(() => {
76
+ // A failed close leaves admission terminal, but residual work may settle later.
77
+ // Let the same owner join that cleanup on its next close attempt.
78
+ if (this.closing === flight)
79
+ this.closing = undefined;
80
+ });
81
+ }
67
82
  return this.closing;
68
83
  }
69
84
  async closeOnce(deadline) {
70
85
  await this.waitUntilIdleOr(deadline);
86
+ const hasCancellationWork = [...this.operations].some((operation) => operation.hasCancellationWork);
71
87
  if (this.operations.size > 0) {
72
88
  for (const operation of this.operations)
73
89
  operation.cancel('server closing');
74
90
  }
75
91
  this.closeSockets();
76
- // Give cooperative cancellation and iterator returns one turn to settle. The absolute deadline
77
- // has already elapsed; an uncooperative operation must reject close rather than pin Host forever.
78
- await Promise.resolve();
92
+ if (hasCancellationWork) {
93
+ await this.waitUntilIdleOr(Date.now() + CANCELLATION_CLEANUP_GRACE_MS);
94
+ }
95
+ else {
96
+ await Promise.resolve();
97
+ }
79
98
  if (this.operations.size > 0 || this.backgroundTasks.size > 0) {
80
99
  this.closed = true;
81
100
  throw new Error(`Server close deadline elapsed with ${this.operations.size} active operation(s) and ${this.backgroundTasks.size} background task(s).`);
@@ -110,11 +129,11 @@ export class ActiveOperations {
110
129
  }
111
130
  }
112
131
  class Operation {
113
- deadline;
114
132
  onSettled;
115
133
  controller = new AbortController();
116
134
  cancelWork = new Set();
117
135
  timer;
136
+ deadlineValue;
118
137
  external;
119
138
  settled = false;
120
139
  cancelled = false;
@@ -122,15 +141,31 @@ class Operation {
122
141
  resolveFinished;
123
142
  finished;
124
143
  constructor(deadline, onSettled) {
125
- this.deadline = deadline;
126
144
  this.onSettled = onSettled;
145
+ this.deadlineValue = deadline;
127
146
  this.finished = new Promise((resolve) => {
128
147
  this.resolveFinished = resolve;
129
148
  });
130
- this.timer = setTimeout(() => {
149
+ this.timer = this.arm();
150
+ }
151
+ get deadline() {
152
+ return this.deadlineValue;
153
+ }
154
+ extend(timeoutMs) {
155
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2_147_483_647) {
156
+ throw new TypeError('Active operation timeout is invalid.');
157
+ }
158
+ if (this.cancelled || this.settled)
159
+ return;
160
+ clearTimeout(this.timer);
161
+ this.deadlineValue = Date.now() + timeoutMs;
162
+ this.timer = this.arm();
163
+ }
164
+ arm() {
165
+ return setTimeout(() => {
131
166
  this.timeout = true;
132
167
  this.cancel('invocation deadline exceeded');
133
- }, Math.min(Math.max(0, deadline - Date.now()), 2_147_483_647));
168
+ }, Math.min(Math.max(0, this.deadlineValue - Date.now()), 2_147_483_647));
134
169
  }
135
170
  link(signal) {
136
171
  if (signal === undefined)
@@ -148,6 +183,9 @@ class Operation {
148
183
  get timedOut() {
149
184
  return this.timeout;
150
185
  }
186
+ get hasCancellationWork() {
187
+ return this.cancelWork.size > 0;
188
+ }
151
189
  onCancel(cancel) {
152
190
  if (this.cancelled) {
153
191
  void settleCancellation(cancel, abortReason(this.controller.signal));
@@ -197,4 +235,3 @@ async function settleCancellation(cancel, reason) {
197
235
  function abortReason(signal) {
198
236
  return signal.reason instanceof Error ? signal.reason.message : 'operation cancelled';
199
237
  }
200
- //# sourceMappingURL=active.js.map
@@ -1,2 +1 @@
1
1
  export { ActiveOperations, type ActiveOperation, type ActiveSocket } from './active.js';
2
- //# sourceMappingURL=index.d.ts.map
@@ -1,2 +1 @@
1
1
  export { ActiveOperations } from './active.js';
2
- //# sourceMappingURL=index.js.map
@@ -0,0 +1,9 @@
1
+ import type { ActiveOperations } from '../../lifecycle/index.js';
2
+ import type { Served } from '../served.js';
3
+ import type { Provider } from './types.js';
4
+ export interface Limits {
5
+ readonly maximumDeliveryBytes: number;
6
+ readonly invocationTimeoutMs: number;
7
+ }
8
+ /** Serve one version-two Delivery against the exact snapshot that selected its URL. */
9
+ export declare function handleDelivery(request: Request, snapshot: Served, provider: Provider | undefined, active: ActiveOperations, limits: Limits, corsFor: (exposedHeaders?: readonly string[]) => Readonly<Record<string, string>>): Promise<Response>;
@@ -1,23 +1,29 @@
1
- import { codecs, publication as protocolPublication } from '@astrale-os/kernel-protocol';
2
- import { DeliveryUnavailableError } from '../../errors/index.js';
1
+ import * as codecs from '@astrale-os/kernel-protocol/codecs';
2
+ import * as release from '@astrale-os/kernel-protocol/release';
3
+ import { admissionRefusal, DeliveryUnavailableError, retryAfterHeaders, } from '../../errors/index.js';
3
4
  import { RequestBodyInvalidError, RequestBodyTooLargeError } from '../../errors/index.js';
4
5
  import { readBoundedBody } from '../../transport/index.js';
5
- const delivery = protocolPublication.delivery;
6
- /** Serve one delivery request against the exact Publication snapshot that selected its URL. */
7
- export async function handleDelivery(request, snapshot, provider, active, limits, cors) {
6
+ import { servedDocument } from '../served.js';
7
+ const delivery = release.delivery;
8
+ /** Serve one version-two Delivery against the exact snapshot that selected its URL. */
9
+ export async function handleDelivery(request, snapshot, provider, active, limits, corsFor) {
10
+ const cors = corsFor();
8
11
  if (request.method !== 'POST') {
9
12
  return response({ error: 'Delivery requires POST.' }, 405, cors);
10
13
  }
11
14
  if (provider === undefined) {
12
15
  return response({ error: 'Delivery is unavailable.' }, 503, cors);
13
16
  }
14
- const advertised = snapshot.publication.delivery;
17
+ const advertised = servedDocument(snapshot).delivery;
15
18
  if (advertised === undefined) {
16
19
  return response({ error: 'Delivery is unavailable.' }, 404, cors);
17
20
  }
18
21
  const operation = active.begin(limits.invocationTimeoutMs, request.signal);
19
- if (operation === undefined)
20
- return response({ error: 'Server is draining.' }, 503, cors);
22
+ if (operation === undefined) {
23
+ const refusal = admissionRefusal(active.isAccepting);
24
+ const retry = retryAfterHeaders(refusal);
25
+ return response({ error: refusal.message }, 503, { ...corsFor(Object.keys(retry)), ...retry });
26
+ }
21
27
  try {
22
28
  const authorization = request.headers.get('authorization');
23
29
  const token = bearer(authorization);
@@ -63,8 +69,9 @@ export async function handleDelivery(request, snapshot, provider, active, limits
63
69
  }
64
70
  throw cause;
65
71
  }
66
- if (input.etag !== snapshot.publication.etag) {
67
- return response({ error: 'Publication ETag differs from request.' }, 409, cors);
72
+ const served = snapshot.release.digest;
73
+ if (input.release !== served) {
74
+ return response({ error: 'Release digest differs from request.' }, 409, cors);
68
75
  }
69
76
  let candidate;
70
77
  try {
@@ -89,8 +96,8 @@ export async function handleDelivery(request, snapshot, provider, active, limits
89
96
  }
90
97
  throw cause;
91
98
  }
92
- if (created.publication.etag !== snapshot.publication.etag) {
93
- return response({ error: 'Delivery provider returned a different Publication.' }, 500, cors);
99
+ if (created.release.digest !== served) {
100
+ return response({ error: 'Delivery provider returned a different release.' }, 500, cors);
94
101
  }
95
102
  const encoded = codecs.json.encode(created);
96
103
  if (encodedSize(encoded) > limits.maximumDeliveryBytes) {
@@ -124,4 +131,3 @@ function encodedSize(input) {
124
131
  function toBody(input) {
125
132
  return input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength);
126
133
  }
127
- //# sourceMappingURL=handler.js.map
@@ -0,0 +1 @@
1
+ export type { Context, Provider } from './types.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -1,12 +1,11 @@
1
- import type { publication } from '@astrale-os/kernel-protocol';
1
+ import type * as release from '@astrale-os/kernel-protocol/release';
2
2
  export interface Context {
3
3
  /** Omitted for a public endpoint; otherwise the exact admitted retrieval credential. */
4
- readonly token?: publication.delivery.Token;
4
+ readonly token?: release.delivery.Token;
5
5
  readonly signal: AbortSignal;
6
6
  }
7
- /** Provider-owned retrieval, construction, and attestation signing. */
7
+ /** Provider-owned retrieval, construction, and attestation signing for the served release. */
8
8
  export interface Provider {
9
9
  /** Throw DeliveryUnavailableError only for an expected dependency outage. */
10
- create(request: publication.delivery.Request, context: Context): Promise<publication.delivery.Delivery>;
10
+ create(request: release.delivery.Request, context: Context): Promise<release.delivery.Delivery>;
11
11
  }
12
- //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export { prepareReleaseSnapshot } from './snapshot.js';
2
+ export type { ReleaseSnapshot, ReleaseSnapshotInput, ReleaseSource } from './types.js';
3
+ export * as delivery from './delivery/index.js';
@@ -0,0 +1,2 @@
1
+ export { prepareReleaseSnapshot } from './snapshot.js';
2
+ export * as delivery from './delivery/index.js';
@@ -0,0 +1,21 @@
1
+ import type { IssuerId } from '@astrale-os/kernel-core/auth/issuer';
2
+ import type { DomainRelease } from '@astrale-os/kernel-protocol/release';
3
+ import type { PreparedReleaseSnapshot } from './snapshot.js';
4
+ import type { ReleaseSnapshot, ReleaseSource } from './types.js';
5
+ export interface ServingSource {
6
+ readonly kind: 'release';
7
+ readonly source: ReleaseSource;
8
+ }
9
+ export type Served = PreparedReleaseSnapshot;
10
+ export interface RequestServing {
11
+ readonly kind: 'release';
12
+ /** The snapshot the request pins; undefined while none is ready or it is withheld. */
13
+ current(): Served | undefined;
14
+ /** The public serving origin named by a DEPLOYMENT_RETIRED answer. */
15
+ deployment(): IssuerId;
16
+ }
17
+ export declare function readServed(serving: ServingSource): Served | undefined;
18
+ export declare function servedDocument(served: Served): DomainRelease;
19
+ export declare function pinned(served: Served): {
20
+ readonly release: ReleaseSnapshot;
21
+ };
@@ -0,0 +1,10 @@
1
+ import { readPreparedReleaseSnapshot } from './snapshot.js';
2
+ export function readServed(serving) {
3
+ return readPreparedReleaseSnapshot(serving.source.current());
4
+ }
5
+ export function servedDocument(served) {
6
+ return served.release;
7
+ }
8
+ export function pinned(served) {
9
+ return { release: served.snapshot };
10
+ }
@@ -0,0 +1,22 @@
1
+ import type { Domain } from '@astrale-os/kernel-dsl/v1';
2
+ import type { ServedReferences, Table } from '@astrale-os/kernel-protocol/routes';
3
+ import { type DomainRelease } from '@astrale-os/kernel-protocol/release';
4
+ import type { ReleaseSnapshot, ReleaseSnapshotInput } from './types.js';
5
+ export interface PreparedReleaseSnapshot {
6
+ readonly kind: 'release';
7
+ readonly snapshot: ReleaseSnapshot;
8
+ readonly release: DomainRelease;
9
+ readonly releaseBytes: Uint8Array;
10
+ /** The exact release reference native-route If-Match names. */
11
+ readonly references: ServedReferences;
12
+ readonly bundle: Uint8Array;
13
+ readonly table: Table;
14
+ readonly domain: Domain;
15
+ }
16
+ /**
17
+ * Admit and detach one deployment's serving snapshot before it is served: the release, its exact
18
+ * bundle bytes, its resolved Domain and route Table.
19
+ */
20
+ export declare function prepareReleaseSnapshot(input: ReleaseSnapshotInput): ReleaseSnapshot;
21
+ /** Internal proof that a ReleaseSource returned a Server-prepared snapshot without cloning it. */
22
+ export declare function readPreparedReleaseSnapshot(input: unknown): PreparedReleaseSnapshot | undefined;
@@ -0,0 +1,49 @@
1
+ import { accept as acceptRelease, bundleUrl, encode as encodeRelease, verifyBundleBytes, } from '@astrale-os/kernel-protocol/release';
2
+ import { admitTable } from './table.js';
3
+ const snapshots = new WeakMap();
4
+ /**
5
+ * Admit and detach one deployment's serving snapshot before it is served: the release, its exact
6
+ * bundle bytes, its resolved Domain and route Table.
7
+ */
8
+ export function prepareReleaseSnapshot(input) {
9
+ if (input === null || typeof input !== 'object' || !(input.bundle instanceof Uint8Array)) {
10
+ throw new TypeError('Release snapshot input is invalid.');
11
+ }
12
+ const release = acceptRelease(input.release);
13
+ const bundleBytes = new Uint8Array(input.bundle);
14
+ verifyBundleBytes(release, bundleBytes);
15
+ const domain = input.domain;
16
+ if (domain.origin !== release.origin || domain.revision !== release.schema.revision) {
17
+ throw new TypeError('Release snapshot Domain identity differs from its release.');
18
+ }
19
+ const table = admitTable(release, input.table, domain);
20
+ const addressed = new URL(bundleUrl(release.identity.issuer, release.schema.bundle.ref));
21
+ const addressedDirectory = addressed.pathname.slice(0, addressed.pathname.lastIndexOf('/') + 1);
22
+ const servingBundle = new URL(release.schema.bundle.href);
23
+ if (servingBundle.origin === addressed.origin &&
24
+ servingBundle.pathname.startsWith(addressedDirectory) &&
25
+ servingBundle.href !== addressed.href) {
26
+ throw new TypeError('Release bundle URL differs from its artifact digest.');
27
+ }
28
+ const snapshot = Object.freeze({ release });
29
+ const prepared = Object.freeze({
30
+ kind: 'release',
31
+ snapshot,
32
+ release,
33
+ releaseBytes: encodeRelease(release),
34
+ references: Object.freeze({
35
+ release: table.release,
36
+ }),
37
+ bundle: bundleBytes,
38
+ table,
39
+ domain,
40
+ });
41
+ snapshots.set(snapshot, prepared);
42
+ return snapshot;
43
+ }
44
+ /** Internal proof that a ReleaseSource returned a Server-prepared snapshot without cloning it. */
45
+ export function readPreparedReleaseSnapshot(input) {
46
+ if (input === null || typeof input !== 'object')
47
+ return undefined;
48
+ return snapshots.get(input);
49
+ }
@@ -0,0 +1,4 @@
1
+ import type { Domain } from '@astrale-os/kernel-dsl/v1';
2
+ import type { DomainRelease } from '@astrale-os/kernel-protocol/release';
3
+ import { type Table } from '@astrale-os/kernel-protocol/routes';
4
+ export declare function admitTable(deployed: DomainRelease, input: Table | undefined, domain: Domain): Table;
@@ -0,0 +1,11 @@
1
+ import { reference } from '@astrale-os/kernel-protocol/release';
2
+ import { acceptTable, verifyTable } from '@astrale-os/kernel-protocol/routes';
3
+ export function admitTable(deployed, input, domain) {
4
+ if (input === undefined) {
5
+ if (deployed.routes.length !== 0) {
6
+ throw new TypeError('A release with native routes requires a Route Table.');
7
+ }
8
+ return verifyTable(Object.freeze({ release: reference(deployed), entries: Object.freeze([]) }), deployed, domain);
9
+ }
10
+ return verifyTable(acceptTable(input), deployed, domain);
11
+ }
@@ -0,0 +1,23 @@
1
+ import type { Domain } from '@astrale-os/kernel-dsl/v1';
2
+ import type * as release from '@astrale-os/kernel-protocol/release';
3
+ import type { Table } from '@astrale-os/kernel-protocol/routes';
4
+ declare const RELEASE_SNAPSHOT: unique symbol;
5
+ export interface ReleaseSnapshotInput {
6
+ readonly release: release.DomainRelease;
7
+ /** Exact artifact served at DomainRelease.schema.bundle.href. */
8
+ readonly bundle: Uint8Array;
9
+ /** Route Table bound to this release; required when it declares native routes. */
10
+ readonly table?: Table;
11
+ readonly domain: Domain;
12
+ }
13
+ /** Immutable admitted serving identity; bundle and compiled routes remain Server-private. */
14
+ export interface ReleaseSnapshot {
15
+ readonly release: release.DomainRelease;
16
+ readonly [RELEASE_SNAPSHOT]: true;
17
+ }
18
+ /** Atomically readable source swapped only after a complete snapshot is prepared. */
19
+ export interface ReleaseSource {
20
+ /** Undefined until the first snapshot is ready. */
21
+ current(): ReleaseSnapshot | undefined;
22
+ }
23
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ /** Match a resource path and query against its trusted serving origin. */
2
+ export declare function sameRoute(left: string, right: string, externalOrigin?: string): boolean;
@@ -0,0 +1,6 @@
1
+ /** Match a resource path and query against its trusted serving origin. */
2
+ export function sameRoute(left, right, externalOrigin) {
3
+ const a = new URL(left);
4
+ const b = new URL(right);
5
+ return ((externalOrigin ?? a.origin) === b.origin && a.pathname === b.pathname && a.search === b.search);
6
+ }
@@ -0,0 +1,9 @@
1
+ import type { BinaryOutput, StreamingBinaryResult } from '@astrale-os/kernel-core/callable';
2
+ import type { CorsPolicy } from '../cors/index.js';
3
+ import type { ActiveOperation } from '../lifecycle/index.js';
4
+ /** Retain the native operation until its byte producer ends or is closed. */
5
+ export declare function streamingBinaryResponse(binary: StreamingBinaryResult, operation: ActiveOperation, cors: CorsPolicy, request: Request): Response;
6
+ /** Identify a producer-backed binary result before handing off operation ownership. */
7
+ export declare function isStreamingBinary(input: BinaryOutput): input is StreamingBinaryResult;
8
+ /** Render buffered application bytes with the native binary header manifest. */
9
+ export declare function binaryResponse(binary: Exclude<BinaryOutput, StreamingBinaryResult>, cors: CorsPolicy, request: Request): Response;