@astrale-os/kernel-server 0.5.0-beta.7 → 0.5.0-beta.71

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 (215) 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 +8 -4
  22. package/dist/index.js +7 -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/legacy/index.d.ts +20 -0
  30. package/dist/invocation/legacy/index.js +20 -0
  31. package/dist/invocation/{publication-route.d.ts → legacy/publication-route.d.ts} +7 -5
  32. package/dist/invocation/{publication-route.js → legacy/publication-route.js} +7 -13
  33. package/dist/invocation/release-route.d.ts +26 -0
  34. package/dist/invocation/release-route.js +55 -0
  35. package/dist/invocation/types.d.ts +20 -7
  36. package/dist/invocation/types.js +0 -1
  37. package/dist/issuer/exchange.d.ts +0 -1
  38. package/dist/issuer/exchange.js +20 -10
  39. package/dist/issuer/index.d.ts +0 -1
  40. package/dist/issuer/index.js +0 -1
  41. package/dist/issuer/routes.d.ts +0 -1
  42. package/dist/issuer/routes.js +33 -5
  43. package/dist/issuer/types.d.ts +5 -1
  44. package/dist/issuer/types.js +0 -1
  45. package/dist/lifecycle/active.d.ts +4 -1
  46. package/dist/lifecycle/active.js +48 -11
  47. package/dist/lifecycle/index.d.ts +0 -1
  48. package/dist/lifecycle/index.js +0 -1
  49. package/dist/release/delivery/handler.d.ts +13 -0
  50. package/dist/{publication → release}/delivery/handler.js +42 -15
  51. package/dist/{publication → release}/delivery/index.d.ts +0 -1
  52. package/dist/{publication → release}/delivery/index.js +0 -1
  53. package/dist/release/delivery/types.d.ts +21 -0
  54. package/dist/release/delivery/types.js +1 -0
  55. package/dist/release/index.d.ts +3 -0
  56. package/dist/release/index.js +2 -0
  57. package/dist/release/legacy/delivery.d.ts +18 -0
  58. package/dist/release/legacy/delivery.js +2 -0
  59. package/dist/release/legacy/index.d.ts +39 -0
  60. package/dist/release/legacy/index.js +38 -0
  61. package/dist/release/legacy/snapshot.d.ts +26 -0
  62. package/dist/release/legacy/snapshot.js +55 -0
  63. package/dist/release/legacy/table.d.ts +5 -0
  64. package/dist/{publication → release/legacy}/table.js +5 -6
  65. package/dist/release/legacy/types.d.ts +31 -0
  66. package/dist/release/legacy/types.js +1 -0
  67. package/dist/release/served.d.ts +30 -0
  68. package/dist/release/served.js +14 -0
  69. package/dist/release/snapshot.d.ts +26 -0
  70. package/dist/release/snapshot.js +54 -0
  71. package/dist/release/table.d.ts +4 -0
  72. package/dist/release/table.js +11 -0
  73. package/dist/release/types.d.ts +33 -0
  74. package/dist/release/types.js +1 -0
  75. package/dist/routes/index.d.ts +1 -2
  76. package/dist/routes/index.js +1 -2
  77. package/dist/routes/routes.d.ts +9 -7
  78. package/dist/routes/routes.js +314 -123
  79. package/dist/server.d.ts +0 -1
  80. package/dist/server.js +110 -48
  81. package/dist/transport/body.d.ts +0 -1
  82. package/dist/transport/body.js +0 -1
  83. package/dist/transport/codecs.d.ts +2 -3
  84. package/dist/transport/codecs.js +4 -4
  85. package/dist/transport/credential.d.ts +0 -1
  86. package/dist/transport/credential.js +0 -1
  87. package/dist/transport/http/binary-request.d.ts +11 -0
  88. package/dist/transport/http/binary-request.js +102 -0
  89. package/dist/transport/http/handler.d.ts +8 -3
  90. package/dist/transport/http/handler.js +95 -45
  91. package/dist/transport/http/index.d.ts +0 -1
  92. package/dist/transport/http/index.js +0 -1
  93. package/dist/transport/http/render.d.ts +9 -4
  94. package/dist/transport/http/render.js +150 -5
  95. package/dist/transport/index.d.ts +0 -1
  96. package/dist/transport/index.js +0 -1
  97. package/dist/transport/router.d.ts +0 -1
  98. package/dist/transport/router.js +0 -1
  99. package/dist/transport/stream-error.d.ts +0 -1
  100. package/dist/transport/stream-error.js +0 -1
  101. package/dist/transport/types.d.ts +1 -2
  102. package/dist/transport/types.js +0 -1
  103. package/dist/transport/websocket/index.d.ts +0 -1
  104. package/dist/transport/websocket/index.js +0 -1
  105. package/dist/transport/websocket/session.d.ts +3 -3
  106. package/dist/transport/websocket/session.js +30 -30
  107. package/dist/transport/websocket/upgrade.d.ts +2 -3
  108. package/dist/transport/websocket/upgrade.js +7 -7
  109. package/dist/types.d.ts +32 -6
  110. package/dist/types.js +0 -1
  111. package/package.json +12 -15
  112. package/dist/capture.d.ts.map +0 -1
  113. package/dist/capture.js.map +0 -1
  114. package/dist/cors/cors.d.ts.map +0 -1
  115. package/dist/cors/cors.js.map +0 -1
  116. package/dist/cors/index.d.ts.map +0 -1
  117. package/dist/cors/index.js.map +0 -1
  118. package/dist/cors/types.d.ts.map +0 -1
  119. package/dist/cors/types.js.map +0 -1
  120. package/dist/errors/availability.d.ts.map +0 -1
  121. package/dist/errors/availability.js.map +0 -1
  122. package/dist/errors/classify.d.ts.map +0 -1
  123. package/dist/errors/classify.js.map +0 -1
  124. package/dist/errors/index.d.ts.map +0 -1
  125. package/dist/errors/index.js.map +0 -1
  126. package/dist/errors/request-body.d.ts.map +0 -1
  127. package/dist/errors/request-body.js.map +0 -1
  128. package/dist/errors/response-body.d.ts.map +0 -1
  129. package/dist/errors/response-body.js.map +0 -1
  130. package/dist/index.d.ts.map +0 -1
  131. package/dist/index.js.map +0 -1
  132. package/dist/invocation/context.d.ts.map +0 -1
  133. package/dist/invocation/context.js.map +0 -1
  134. package/dist/invocation/identity.d.ts.map +0 -1
  135. package/dist/invocation/identity.js.map +0 -1
  136. package/dist/invocation/index.d.ts.map +0 -1
  137. package/dist/invocation/index.js.map +0 -1
  138. package/dist/invocation/publication-route.d.ts.map +0 -1
  139. package/dist/invocation/publication-route.js.map +0 -1
  140. package/dist/invocation/types.d.ts.map +0 -1
  141. package/dist/invocation/types.js.map +0 -1
  142. package/dist/issuer/exchange.d.ts.map +0 -1
  143. package/dist/issuer/exchange.js.map +0 -1
  144. package/dist/issuer/index.d.ts.map +0 -1
  145. package/dist/issuer/index.js.map +0 -1
  146. package/dist/issuer/routes.d.ts.map +0 -1
  147. package/dist/issuer/routes.js.map +0 -1
  148. package/dist/issuer/types.d.ts.map +0 -1
  149. package/dist/issuer/types.js.map +0 -1
  150. package/dist/lifecycle/active.d.ts.map +0 -1
  151. package/dist/lifecycle/active.js.map +0 -1
  152. package/dist/lifecycle/index.d.ts.map +0 -1
  153. package/dist/lifecycle/index.js.map +0 -1
  154. package/dist/publication/delivery/handler.d.ts +0 -10
  155. package/dist/publication/delivery/handler.d.ts.map +0 -1
  156. package/dist/publication/delivery/handler.js.map +0 -1
  157. package/dist/publication/delivery/index.d.ts.map +0 -1
  158. package/dist/publication/delivery/index.js.map +0 -1
  159. package/dist/publication/delivery/types.d.ts +0 -12
  160. package/dist/publication/delivery/types.d.ts.map +0 -1
  161. package/dist/publication/delivery/types.js +0 -2
  162. package/dist/publication/delivery/types.js.map +0 -1
  163. package/dist/publication/index.d.ts +0 -4
  164. package/dist/publication/index.d.ts.map +0 -1
  165. package/dist/publication/index.js +0 -3
  166. package/dist/publication/index.js.map +0 -1
  167. package/dist/publication/snapshot.d.ts +0 -16
  168. package/dist/publication/snapshot.d.ts.map +0 -1
  169. package/dist/publication/snapshot.js +0 -39
  170. package/dist/publication/snapshot.js.map +0 -1
  171. package/dist/publication/table.d.ts +0 -4
  172. package/dist/publication/table.d.ts.map +0 -1
  173. package/dist/publication/table.js.map +0 -1
  174. package/dist/publication/types.d.ts +0 -23
  175. package/dist/publication/types.d.ts.map +0 -1
  176. package/dist/publication/types.js +0 -2
  177. package/dist/publication/types.js.map +0 -1
  178. package/dist/publication/validator.d.ts +0 -4
  179. package/dist/publication/validator.d.ts.map +0 -1
  180. package/dist/publication/validator.js +0 -17
  181. package/dist/publication/validator.js.map +0 -1
  182. package/dist/routes/index.d.ts.map +0 -1
  183. package/dist/routes/index.js.map +0 -1
  184. package/dist/routes/routes.d.ts.map +0 -1
  185. package/dist/routes/routes.js.map +0 -1
  186. package/dist/server.d.ts.map +0 -1
  187. package/dist/server.js.map +0 -1
  188. package/dist/transport/body.d.ts.map +0 -1
  189. package/dist/transport/body.js.map +0 -1
  190. package/dist/transport/codecs.d.ts.map +0 -1
  191. package/dist/transport/codecs.js.map +0 -1
  192. package/dist/transport/credential.d.ts.map +0 -1
  193. package/dist/transport/credential.js.map +0 -1
  194. package/dist/transport/http/handler.d.ts.map +0 -1
  195. package/dist/transport/http/handler.js.map +0 -1
  196. package/dist/transport/http/index.d.ts.map +0 -1
  197. package/dist/transport/http/index.js.map +0 -1
  198. package/dist/transport/http/render.d.ts.map +0 -1
  199. package/dist/transport/http/render.js.map +0 -1
  200. package/dist/transport/index.d.ts.map +0 -1
  201. package/dist/transport/index.js.map +0 -1
  202. package/dist/transport/router.d.ts.map +0 -1
  203. package/dist/transport/router.js.map +0 -1
  204. package/dist/transport/stream-error.d.ts.map +0 -1
  205. package/dist/transport/stream-error.js.map +0 -1
  206. package/dist/transport/types.d.ts.map +0 -1
  207. package/dist/transport/types.js.map +0 -1
  208. package/dist/transport/websocket/index.d.ts.map +0 -1
  209. package/dist/transport/websocket/index.js.map +0 -1
  210. package/dist/transport/websocket/session.d.ts.map +0 -1
  211. package/dist/transport/websocket/session.js.map +0 -1
  212. package/dist/transport/websocket/upgrade.d.ts.map +0 -1
  213. package/dist/transport/websocket/upgrade.js.map +0 -1
  214. package/dist/types.d.ts.map +0 -1
  215. package/dist/types.js.map +0 -1
@@ -1,57 +1,46 @@
1
1
  import * as codecs from '@astrale-os/kernel-protocol/codecs';
2
2
  import * as errors from '@astrale-os/kernel-protocol/errors';
3
3
  import * as protocol from '@astrale-os/kernel-protocol/invocation';
4
- import { PATH as PUBLICATION_PATH, url as publicationUrl, } from '@astrale-os/kernel-protocol/publication/publication';
5
- import { reference as publicationReference } from '@astrale-os/kernel-protocol/publication/reference';
4
+ import { bundleUrl, MEDIA_TYPE as RELEASE_MEDIA_TYPE, PATH as RELEASE_PATH, url as releaseUrl, } from '@astrale-os/kernel-protocol/release';
5
+ import { mediaType as publicationMediaType, PATH as PUBLICATION_PATH, reference as publicationReference, url as publicationUrl, } from '@astrale-os/kernel-protocol/release/legacy';
6
6
  import * as routes from '@astrale-os/kernel-protocol/routes';
7
- import { classifyError, RequestBodyInvalidError, RequestBodyTooLargeError, } from '../errors/index.js';
7
+ import { admissionRefusal, classifyError, classifyRequestError, RequestBodyInvalidError, RequestBodyTooLargeError, retryAfterHeaders, } from '../errors/index.js';
8
8
  import { invocationContext } from '../invocation/index.js';
9
- import { handleDelivery } from '../publication/delivery/index.js';
10
- import { readPreparedPublicationSnapshot, } from '../publication/index.js';
9
+ import { handleDelivery } from '../release/delivery/index.js';
10
+ import { pinned, readServed, servedDocument } from '../release/served.js';
11
11
  import { readBoundedBody } from '../transport/body.js';
12
12
  import {} from '../transport/router.js';
13
13
  import { interruptedStreamError } from '../transport/stream-error.js';
14
- export function mountRoutes(app, source, deliveryProvider, handler, active, nextInvocation, limits, cors, hostFor, reservations, externalIssuer) {
15
- const publicationPath = externalIssuer === undefined
16
- ? PUBLICATION_PATH
17
- : new URL(publicationUrl(externalIssuer)).pathname;
18
- const externalOrigin = externalIssuer === undefined ? undefined : new URL(externalIssuer).origin;
19
- if (cors.enabled) {
20
- app.options(publicationPath, (context) => cors.preflight(context.req.raw, {
21
- methods: ['GET'],
22
- headers: ['if-none-match'],
23
- }));
24
- }
25
- app.get(publicationPath, (context) => {
26
- const responseCors = cors.response(context.req.raw, ['etag']);
27
- if (!active.isAccepting)
28
- return response({ error: 'Publication is unavailable while draining.' }, 503, responseCors);
29
- const snapshot = servingSnapshot(source, limits, context.req.raw, reservations, externalIssuer);
30
- if (snapshot === undefined)
31
- return response({ error: 'Publication is not ready.' }, 503, responseCors);
32
- const body = snapshot.publicationBytes;
33
- if (context.req.raw.headers.get('if-none-match') === quoteEtag(snapshot.publication.etag)) {
34
- return new Response(null, {
35
- status: 304,
36
- headers: {
37
- ...responseCors,
38
- etag: quoteEtag(snapshot.publication.etag),
39
- 'cache-control': 'public, max-age=0, must-revalidate, no-transform',
40
- },
41
- });
42
- }
43
- return new Response(toBody(body), {
44
- status: 200,
45
- headers: {
46
- ...responseCors,
47
- 'content-type': 'application/json',
48
- etag: quoteEtag(snapshot.publication.etag),
49
- 'cache-control': 'public, max-age=0, must-revalidate, no-transform',
50
- },
14
+ export function mountRoutes(app, source, deliveryProvider, handler, active, nextInvocation, limits, cors, hostFor, reservations, externalIssuer, publicOrigin) {
15
+ const externalOrigin = publicOrigin ?? (externalIssuer === undefined ? undefined : new URL(externalIssuer).origin);
16
+ const served = (request) => servingSnapshot(source, limits, request, reservations, externalIssuer, externalOrigin);
17
+ if (source.kind === 'release') {
18
+ mountDocument(app, documentPath(RELEASE_PATH, releaseUrl, externalIssuer), 'Release', {
19
+ cors,
20
+ active,
21
+ served,
22
+ read: (snapshot) => snapshot.kind === 'release'
23
+ ? {
24
+ bytes: snapshot.releaseBytes,
25
+ mediaType: RELEASE_MEDIA_TYPE,
26
+ etag: snapshot.release.digest,
27
+ }
28
+ : undefined,
51
29
  });
30
+ }
31
+ // At a release-backed destination `domain.json` serves the frozen v3 projection.
32
+ mountDocument(app, documentPath(PUBLICATION_PATH, publicationUrl, externalIssuer), 'Publication', {
33
+ cors,
34
+ active,
35
+ served,
36
+ read: (snapshot) => ({
37
+ bytes: snapshot.publicationBytes,
38
+ mediaType: publicationMediaType(snapshot.publication),
39
+ etag: snapshot.publication.etag,
40
+ }),
52
41
  });
53
42
  app.all('*', async (context) => {
54
- const snapshot = servingSnapshot(source, limits, context.req.raw, reservations, externalIssuer);
43
+ const snapshot = served(context.req.raw);
55
44
  if (snapshot === undefined)
56
45
  return context.notFound();
57
46
  const request = context.req.raw;
@@ -59,48 +48,80 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
59
48
  return publicationPreflight(cors, request, snapshot, externalOrigin);
60
49
  }
61
50
  const responseCors = cors.response(request);
62
- if (sameRoute(request.url, snapshot.publication.schema.bundle.href, externalOrigin)) {
63
- if (!active.isAccepting)
64
- return response({ error: 'Publication artifact is unavailable while draining.' }, 503, responseCors);
51
+ const corsFor = (exposed) => cors.response(request, exposed);
52
+ const document = servedDocument(snapshot);
53
+ const descriptor = document.schema.bundle;
54
+ const addressed = bundleUrl(document.identity.issuer, descriptor.ref);
55
+ const immutable = descriptor.href === addressed;
56
+ if (sameRoute(request.url, descriptor.href, externalOrigin)) {
57
+ // A release names its own document; a v3-input destination keeps its recorded bytes.
58
+ const releaseBacked = snapshot.kind === 'release';
59
+ if (!active.isAccepting) {
60
+ return drained(releaseBacked
61
+ ? 'Release artifact is unavailable while draining.'
62
+ : 'Publication artifact is unavailable while draining.', corsFor);
63
+ }
65
64
  if (request.method !== 'GET')
66
65
  return response({ error: 'Bundle retrieval requires GET.' }, 405, responseCors);
67
66
  const body = snapshot.bundle;
68
- if (body.byteLength !== snapshot.publication.schema.bundle.ref.size) {
69
- return response({ error: 'Bundle differs from its Publication.' }, 500, responseCors);
67
+ if (body.byteLength !== descriptor.ref.size) {
68
+ return response({
69
+ error: releaseBacked
70
+ ? 'Bundle differs from its release.'
71
+ : 'Bundle differs from its Publication.',
72
+ }, 500, responseCors);
70
73
  }
71
- return new Response(toBody(body), {
72
- status: 200,
74
+ const etag = quoteEtag(descriptor.ref.digest);
75
+ const unchanged = request.headers.get('if-none-match') === etag;
76
+ return new Response(unchanged ? null : toBody(body), {
77
+ status: unchanged ? 304 : 200,
73
78
  headers: {
74
79
  ...responseCors,
75
- 'content-type': snapshot.publication.schema.bundle.ref.mediaType,
76
- etag: quoteEtag(snapshot.publication.schema.bundle.ref.digest),
77
- 'cache-control': 'public, max-age=31536000, immutable, no-transform',
80
+ 'content-type': descriptor.ref.mediaType,
81
+ etag,
82
+ 'cache-control': immutable
83
+ ? 'public, max-age=31536000, immutable, no-transform'
84
+ : 'public, max-age=0, must-revalidate, no-transform',
78
85
  },
79
86
  });
80
87
  }
81
- const deliveryUrl = snapshot.publication.delivery?.href;
88
+ if (request.method === 'GET' && immutable) {
89
+ const pathname = new URL(addressed).pathname;
90
+ const directory = pathname.slice(0, pathname.lastIndexOf('/') + 1);
91
+ const requested = new URL(request.url);
92
+ if (requested.pathname.startsWith(directory) &&
93
+ /^[0-9a-f]{64}\.json$/u.test(requested.pathname.slice(directory.length))) {
94
+ return new Response(null, {
95
+ status: 404,
96
+ headers: { ...responseCors, 'cache-control': 'no-store' },
97
+ });
98
+ }
99
+ }
100
+ const deliveryUrl = document.delivery?.href;
82
101
  if (deliveryUrl !== undefined && sameRoute(request.url, deliveryUrl, externalOrigin)) {
83
- return handleDelivery(request, snapshot, deliveryProvider, active, limits, cors.response(request));
102
+ return handleDelivery(request, snapshot, deliveryProvider, active, limits, corsFor);
84
103
  }
85
104
  let routing;
86
105
  try {
87
- const ifMatch = request.headers.get(routes.HTTP_ROUTE_PUBLICATION);
88
- if (ifMatch !== null &&
89
- /^"sha256:[0-9a-f]{64}"$/u.test(ifMatch) &&
90
- ifMatch !== quoteEtag(snapshot.publication.etag)) {
91
- return new Response(null, {
92
- status: routes.HTTP_ROUTE_STALE_STATUS,
93
- headers: {
94
- ...cors.response(request, [routes.HTTP_ROUTE_STALE]),
95
- ...routes.staleHeaders(snapshot.publication.etag),
96
- },
97
- });
106
+ if (snapshot.kind === 'release') {
107
+ const decoded = routes.decodeRouting(request, snapshot.references);
108
+ if (decoded?.kind === 'stale')
109
+ return stale(request, decoded.current, cors);
110
+ routing = decoded;
111
+ }
112
+ else {
113
+ const ifMatch = request.headers.get(routes.HTTP_ROUTE_DIGEST);
114
+ if (ifMatch !== null &&
115
+ /^"sha256:[0-9a-f]{64}"$/u.test(ifMatch) &&
116
+ ifMatch !== quoteEtag(snapshot.publication.etag)) {
117
+ return stale(request, snapshot.publication.etag, cors);
118
+ }
119
+ routing = routes.legacy.decodeRouting(request, publicationReference(snapshot.publication));
98
120
  }
99
- routing = routes.decodeRouting(request, publicationReference(snapshot.publication));
100
121
  }
101
122
  catch (cause) {
102
123
  if (cause instanceof routes.RouteError) {
103
- return nativeError({ code: errors.CODES.INVALID_REQUEST, message: 'Native route routing is invalid.' }, 400, responseCors);
124
+ return nativeError({ code: errors.CODES.INVALID_REQUEST, message: 'Native route routing is invalid.' }, 400, corsFor);
104
125
  }
105
126
  throw cause;
106
127
  }
@@ -110,14 +131,23 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
110
131
  const assigned = nextInvocation();
111
132
  const operation = active.begin(limits.invocationTimeoutMs, request.signal);
112
133
  if (operation === undefined)
113
- return nativeError({
114
- code: errors.CODES.BACKEND_UNAVAILABLE,
115
- message: 'Server is draining.',
116
- }, 503, responseCors);
134
+ return nativeError(admissionRefusal(active.isAccepting), 503, corsFor);
117
135
  let handedOff = false;
118
136
  try {
119
137
  const body = await readBoundedBody(request, limits.maximumRequestBytes, operation.signal);
120
- const invocation = routes.decode(route, { url: request.url, method: request.method, headers: request.headers, body }, snapshot.validator, snapshot.table.publication);
138
+ const native = { url: request.url, method: request.method, headers: request.headers, body };
139
+ const invocation = snapshot.kind === 'release'
140
+ ? routes.decode(route, native, snapshot.domain, snapshot.references)
141
+ : routes.legacy.decode(route, native, snapshot.domain, snapshot.table.publication);
142
+ if (invocation.credentialIn === 'cookie' &&
143
+ isStateChanging(request) &&
144
+ isClearlyCrossSite(request, externalOrigin ?? new URL(request.url).origin)) {
145
+ const refusal = {
146
+ code: errors.CODES.ACCESS_DENIED,
147
+ message: 'Cross-site cookie-authenticated request to a native route is refused.',
148
+ };
149
+ return nativeError(refusal, errors.httpStatusOf(refusal), corsFor);
150
+ }
121
151
  const idempotencyHeader = request.headers.get(protocol.HTTP_IDEMPOTENCY_KEY);
122
152
  const idempotencyKey = idempotencyHeader === null
123
153
  ? undefined
@@ -126,7 +156,7 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
126
156
  call: invocation.call,
127
157
  ...(invocation.credential === undefined ? {} : { credential: invocation.credential }),
128
158
  ...(routing === undefined ? {} : { via: routing }),
129
- publication: snapshot.snapshot,
159
+ ...pinned(snapshot),
130
160
  context: invocationContext(assigned, operation, active, hostFor(request), body, idempotencyKey),
131
161
  }));
132
162
  if (result.kind === 'value') {
@@ -138,8 +168,17 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
138
168
  });
139
169
  }
140
170
  if (result.kind === 'binary') {
171
+ if (isStreamingBinary(result.value)) {
172
+ // Hand the operation off only once the streaming response is built. A refused render
173
+ // (reserved header or a body-forbidding status) throws before this point, so the
174
+ // `finally` below still settles the operation instead of leaking its slot.
175
+ const streaming = streamingBinaryResponse(result.value, operation, cors, request);
176
+ handedOff = true;
177
+ return streaming;
178
+ }
141
179
  const applicationHeaders = applicationBinaryHeaders(result.value.headers);
142
180
  return new Response(toBody(result.value.body), {
181
+ status: result.value.status ?? 200,
143
182
  headers: {
144
183
  ...cors.response(request, [
145
184
  protocol.http.BINARY_HEADERS_FIELD,
@@ -153,7 +192,7 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
153
192
  }
154
193
  if (result.kind === 'stream') {
155
194
  handedOff = true;
156
- return await streamResponse(result.stream, operation, responseCors);
195
+ return await streamResponse(result.stream, operation, corsFor);
157
196
  }
158
197
  if (result.kind === 'miss') {
159
198
  return new Response(null, {
@@ -167,29 +206,20 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
167
206
  return nativeError({
168
207
  code: errors.CODES.UNSUPPORTED,
169
208
  message: 'Native routes do not forward Runtime redirects.',
170
- }, 502, responseCors);
209
+ }, 502, corsFor);
171
210
  }
172
211
  catch (cause) {
173
212
  if (cause instanceof RequestBodyTooLargeError) {
174
- return nativeError({ code: errors.CODES.LIMIT_EXCEEDED, message: 'Request body is too large.' }, 413, responseCors);
213
+ return nativeError({ code: errors.CODES.LIMIT_EXCEEDED, message: 'Request body is too large.' }, 413, corsFor);
175
214
  }
176
215
  if (cause instanceof RequestBodyInvalidError) {
177
- return nativeError({ code: errors.CODES.INVALID_REQUEST, message: 'Request body is invalid.' }, 400, responseCors);
216
+ return nativeError({ code: errors.CODES.INVALID_REQUEST, message: 'Request body is invalid.' }, 400, corsFor);
178
217
  }
179
218
  if (cause instanceof routes.RouteError) {
180
- return nativeError({ code: errors.CODES.INVALID_REQUEST, message: 'Native route request is invalid.' }, 400, responseCors);
181
- }
182
- if (operation.timedOut) {
183
- return nativeError({
184
- code: errors.CODES.DEADLINE_EXCEEDED,
185
- message: 'Invocation deadline exceeded.',
186
- }, 504, responseCors);
219
+ return nativeError({ code: errors.CODES.INVALID_REQUEST, message: 'Native route request is invalid.' }, 400, corsFor);
187
220
  }
188
- if (operation.signal.aborted) {
189
- return nativeError({ code: errors.CODES.CANCELLED, message: 'Invocation was cancelled.' }, 408, responseCors);
190
- }
191
- const payload = classifyError(cause);
192
- return nativeError(payload, errors.httpStatusOf(payload), responseCors);
221
+ const payload = classifyRequestError(cause, operation);
222
+ return nativeError(payload, errors.httpStatusOf(payload), corsFor);
193
223
  }
194
224
  finally {
195
225
  if (!handedOff)
@@ -197,11 +227,89 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
197
227
  }
198
228
  });
199
229
  }
230
+ function streamingBinaryResponse(binary, operation, cors, request) {
231
+ // Admit the reserved/invalid headers before acquiring the producer, so a refused render never
232
+ // captures the stream it would then have to abandon.
233
+ const applicationHeaders = applicationBinaryHeaders(binary.headers);
234
+ const iterator = binary.body[Symbol.asyncIterator]();
235
+ let terminal = false;
236
+ let controller;
237
+ let stopping;
238
+ const stop = (reason = 'Binary response was interrupted.') => {
239
+ if (terminal)
240
+ return stopping ?? Promise.resolve();
241
+ terminal = true;
242
+ controller?.error(new Error(reason));
243
+ stopping = Promise.resolve().then(async () => {
244
+ try {
245
+ await iterator.return?.();
246
+ }
247
+ finally {
248
+ operation.settle();
249
+ }
250
+ });
251
+ return stopping;
252
+ };
253
+ const body = new ReadableStream({
254
+ start(output) {
255
+ controller = output;
256
+ },
257
+ async pull(controller) {
258
+ if (terminal)
259
+ return;
260
+ try {
261
+ const next = await iterator.next();
262
+ if (terminal)
263
+ return;
264
+ if (next.done) {
265
+ terminal = true;
266
+ controller.close();
267
+ operation.settle();
268
+ return;
269
+ }
270
+ controller.enqueue(next.value);
271
+ }
272
+ catch (cause) {
273
+ if (terminal)
274
+ return;
275
+ terminal = true;
276
+ controller.error(cause);
277
+ operation.settle();
278
+ }
279
+ },
280
+ async cancel(reason) {
281
+ const message = typeof reason === 'string' ? reason : 'native binary response cancelled';
282
+ operation.cancel(message);
283
+ await stop();
284
+ },
285
+ });
286
+ operation.onCancel(stop);
287
+ try {
288
+ return new Response(body, {
289
+ status: binary.status ?? 200,
290
+ headers: {
291
+ ...cors.response(request, Object.keys(applicationHeaders)),
292
+ ...applicationHeaders,
293
+ 'content-type': binary.mediaType,
294
+ },
295
+ });
296
+ }
297
+ catch (cause) {
298
+ // A body-forbidding status (e.g. 204) rejects the Response after the producer was wired:
299
+ // close it and let the caller settle the operation rather than leaking its slot.
300
+ void stop('Binary response was refused.');
301
+ throw cause;
302
+ }
303
+ }
304
+ function isStreamingBinary(input) {
305
+ return !(input.body instanceof Uint8Array);
306
+ }
200
307
  function publicationPreflight(cors, request, snapshot, externalOrigin) {
201
- if (sameRoute(request.url, snapshot.publication.schema.bundle.href, externalOrigin)) {
308
+ const document = servedDocument(snapshot);
309
+ if (sameRoute(request.url, document.schema.bundle.href, externalOrigin)) {
202
310
  return cors.preflight(request, { methods: ['GET'], headers: ['if-none-match'] });
203
311
  }
204
- const deliveryUrl = snapshot.publication.delivery?.href;
312
+ const deliveryUrl = document.delivery?.href;
205
313
  if (deliveryUrl !== undefined && sameRoute(request.url, deliveryUrl, externalOrigin)) {
206
314
  return cors.preflight(request, {
207
315
  methods: ['POST'],
@@ -217,7 +325,7 @@ function publicationPreflight(cors, request, snapshot, externalOrigin) {
217
325
  function nativeCorsRoute(entry, requestedMethod) {
218
326
  const headers = new Set([
219
327
  'accept',
220
- routes.HTTP_ROUTE_PUBLICATION,
328
+ routes.HTTP_ROUTE_DIGEST,
221
329
  routes.HTTP_ROUTE_ISSUER,
222
330
  protocol.HTTP_IDEMPOTENCY_KEY,
223
331
  ]);
@@ -246,23 +354,28 @@ function nativeCorsRoute(entry, requestedMethod) {
246
354
  ...(allowAnyHeaders ? { allowAnyHeaders: true } : {}),
247
355
  });
248
356
  }
249
- export function currentPublicationSnapshot(source, limits, serving) {
250
- const snapshot = source.current();
251
- if (snapshot === undefined)
252
- return undefined;
253
- const prepared = readPreparedPublicationSnapshot(snapshot);
357
+ /**
358
+ * Read the one snapshot a request pins, withheld while it exceeds a serving limit, names another
359
+ * serving issuer, or claims a route this Server reserves.
360
+ */
361
+ export function currentServed(source, limits, serving) {
362
+ const prepared = readServed(source);
254
363
  if (prepared === undefined ||
364
+ (prepared.kind === 'release' &&
365
+ prepared.releaseBytes.byteLength > limits.maximumPublicationBytes) ||
255
366
  prepared.publicationBytes.byteLength > limits.maximumPublicationBytes ||
256
367
  prepared.bundle.byteLength > limits.maximumBundleBytes ||
257
- (serving?.issuer !== undefined && prepared.publication.identity.issuer !== serving.issuer) ||
368
+ (serving?.issuer !== undefined &&
369
+ servedDocument(prepared).identity.issuer !== serving.issuer) ||
258
370
  (serving !== undefined && conflictsWithServingRoutes(prepared, serving))) {
259
371
  return undefined;
260
372
  }
261
373
  return prepared;
262
374
  }
263
- function servingSnapshot(source, limits, request, reservations, externalIssuer) {
264
- return currentPublicationSnapshot(source, limits, {
265
- origin: externalIssuer === undefined ? new URL(request.url).origin : new URL(externalIssuer).origin,
375
+ function servingSnapshot(source, limits, request, reservations, externalIssuer, publicOrigin) {
376
+ return currentServed(source, limits, {
377
+ origin: publicOrigin ??
378
+ (externalIssuer === undefined ? new URL(request.url).origin : new URL(externalIssuer).origin),
266
379
  ...(externalIssuer === undefined ? {} : { issuer: externalIssuer }),
267
380
  reservations,
268
381
  });
@@ -279,6 +392,58 @@ function conflictsWithServingRoutes(snapshot, serving) {
279
392
  }
280
393
  return false;
281
394
  }
395
+ function documentPath(path, url, externalIssuer) {
396
+ return externalIssuer === undefined ? path : new URL(url(externalIssuer)).pathname;
397
+ }
398
+ function mountDocument(app, path, name, serving) {
399
+ const { cors, active } = serving;
400
+ if (cors.enabled) {
401
+ app.options(path, (context) => cors.preflight(context.req.raw, {
402
+ methods: ['GET'],
403
+ headers: ['if-none-match'],
404
+ }));
405
+ }
406
+ app.get(path, (context) => {
407
+ const responseCors = cors.response(context.req.raw, ['etag']);
408
+ if (!active.isAccepting) {
409
+ return drained(`${name} is unavailable while draining.`, (exposed) => cors.response(context.req.raw, exposed));
410
+ }
411
+ const snapshot = serving.served(context.req.raw);
412
+ const document = snapshot === undefined ? undefined : serving.read(snapshot);
413
+ if (document === undefined)
414
+ return response({ error: `${name} is not ready.` }, 503, responseCors);
415
+ const etag = quoteEtag(document.etag);
416
+ if (context.req.raw.headers.get('if-none-match') === etag) {
417
+ return new Response(null, {
418
+ status: 304,
419
+ headers: {
420
+ ...responseCors,
421
+ etag,
422
+ 'cache-control': 'public, max-age=0, must-revalidate, no-transform',
423
+ },
424
+ });
425
+ }
426
+ return new Response(toBody(document.bytes), {
427
+ status: 200,
428
+ headers: {
429
+ ...responseCors,
430
+ 'content-type': document.mediaType,
431
+ etag,
432
+ 'cache-control': 'public, max-age=0, must-revalidate, no-transform',
433
+ },
434
+ });
435
+ });
436
+ }
437
+ /** Reserved native stale control naming the digest the destination serves (`StaleControl.current`). */
438
+ function stale(request, current, cors) {
439
+ return new Response(null, {
440
+ status: routes.HTTP_ROUTE_STALE_STATUS,
441
+ headers: {
442
+ ...cors.response(request, [routes.HTTP_ROUTE_STALE]),
443
+ ...routes.staleHeaders(current),
444
+ },
445
+ });
446
+ }
282
447
  async function streamResponse(stream, operation, cors) {
283
448
  const iterator = stream[Symbol.asyncIterator]();
284
449
  let rejectSetup;
@@ -304,14 +469,14 @@ async function streamResponse(stream, operation, cors) {
304
469
  const cleanup = cancelSetup('native stream setup failed');
305
470
  if (!operation.signal.aborted)
306
471
  await cleanup;
307
- const payload = preStreamError(cause, operation);
472
+ const payload = classifyRequestError(cause, operation);
308
473
  return nativeError(payload, errors.httpStatusOf(payload), cors);
309
474
  }
310
475
  unregisterSetupCancellation();
311
476
  if (operation.signal.aborted) {
312
477
  void cancelSetup('native stream setup cancelled');
313
- const payload = preStreamError(operation.signal.reason, operation);
314
- return response({ error: payload }, errors.httpStatusOf(payload), cors);
478
+ const payload = classifyRequestError(operation.signal.reason, operation);
479
+ return response({ error: payload }, errors.httpStatusOf(payload), cors());
315
480
  }
316
481
  const encoder = new TextEncoder();
317
482
  let pending = first;
@@ -389,32 +554,52 @@ async function streamResponse(stream, operation, cors) {
389
554
  operation.onCancel((reason) => stop(reason, interruptedStreamError(operation.timedOut)));
390
555
  return new Response(body, {
391
556
  headers: {
392
- ...cors,
557
+ ...cors(),
393
558
  'content-type': routes.STREAM_MEDIA_TYPE,
394
559
  'cache-control': 'no-cache',
395
560
  },
396
561
  });
397
562
  }
398
- function preStreamError(cause, operation) {
399
- if (operation.timedOut) {
400
- return {
401
- code: errors.CODES.DEADLINE_EXCEEDED,
402
- message: 'Invocation deadline exceeded before the native stream began.',
403
- };
404
- }
405
- if (operation.signal.aborted) {
406
- return {
407
- code: errors.CODES.CANCELLED,
408
- message: 'Invocation was cancelled before the native stream began.',
409
- };
410
- }
411
- return classifyError(cause);
412
- }
413
563
  function sameRoute(left, right, externalOrigin) {
414
564
  const a = new URL(left);
415
565
  const b = new URL(right);
416
566
  return ((externalOrigin ?? a.origin) === b.origin && a.pathname === b.pathname && a.search === b.search);
417
567
  }
568
+ /**
569
+ * A request that mutates state or navigates to an effectful target. Non-safe HTTP methods always
570
+ * qualify; a GET or HEAD qualifies only as a top-level browser navigation (`Sec-Fetch-Mode:
571
+ * navigate`), the one GET shape that carries SameSite=Lax cookies cross-site. Programmatic GETs
572
+ * (`cors`/`no-cors`) are left untouched so read clients are never broken.
573
+ */
574
+ function isStateChanging(request) {
575
+ switch (request.method.toUpperCase()) {
576
+ case 'POST':
577
+ case 'PUT':
578
+ case 'PATCH':
579
+ case 'DELETE':
580
+ return true;
581
+ case 'GET':
582
+ case 'HEAD':
583
+ return request.headers.get('sec-fetch-mode') === 'navigate';
584
+ default:
585
+ return false;
586
+ }
587
+ }
588
+ /**
589
+ * Positive evidence that a request originates from another site. `Sec-Fetch-Site` is set by the
590
+ * browser and cannot be forged by page script, so when present it is authoritative: only
591
+ * `cross-site` is refused, and `same-origin`/`same-site`/`none` are trusted (a shared-domain
592
+ * subdomain deployment keeps working). When it is absent, a differing `Origin` is the next-best
593
+ * signal. Absence of both is treated as not-cross-site, so same-origin browsers and non-browser
594
+ * clients that omit these headers are never refused.
595
+ */
596
+ function isClearlyCrossSite(request, serviceOrigin) {
597
+ const site = request.headers.get('sec-fetch-site');
598
+ if (site !== null)
599
+ return site === 'cross-site';
600
+ const origin = request.headers.get('origin');
601
+ return origin !== null && origin !== serviceOrigin;
602
+ }
418
603
  function applicationBinaryHeaders(input) {
419
604
  const output = {};
420
605
  for (const [name, value] of Object.entries(input ?? {})) {
@@ -436,9 +621,16 @@ function response(body, status, cors) {
436
621
  headers: { ...cors, 'content-type': 'application/json' },
437
622
  });
438
623
  }
624
+ /** A served document or artifact refused while draining presents the lifecycle retry delay. */
625
+ function drained(message, cors) {
626
+ const retry = retryAfterHeaders(admissionRefusal(false));
627
+ return response({ error: message }, 503, { ...cors(Object.keys(retry)), ...retry });
628
+ }
629
+ /** Every native error, including a capacity refusal raised by dispatch, presents its retry delay. */
439
630
  function nativeError(error, status, cors) {
440
631
  const admitted = routes.decodeErrorResponse({ error });
441
- return response(admitted, status, cors);
632
+ const retry = retryAfterHeaders(error);
633
+ return response(admitted, status, { ...cors(Object.keys(retry)), ...retry });
442
634
  }
443
635
  function encodedBody(input) {
444
636
  return typeof input === 'string' ? input : toBody(input);
@@ -446,4 +638,3 @@ function encodedBody(input) {
446
638
  function toBody(input) {
447
639
  return input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength);
448
640
  }
449
- //# sourceMappingURL=routes.js.map
package/dist/server.d.ts CHANGED
@@ -2,4 +2,3 @@ import type { Server, ServerConfig, ServerOptions, PreparedServer } from './type
2
2
  export declare function createServer(config: ServerConfig): Server;
3
3
  /** Validate all static transport configuration before a Host starts Runtime recovery/bootstrap. */
4
4
  export declare function prepareServer(options: ServerOptions): PreparedServer;
5
- //# sourceMappingURL=server.d.ts.map