@fabrika/proxy-core 0.5.4 → 0.5.5

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 (2) hide show
  1. package/package.json +4 -4
  2. package/src/authorize.ts +21 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabrika/proxy-core",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,9 +37,9 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "zod": "^3.25.76",
40
- "@fabrika/auth-core": "0.5.4",
41
- "@fabrika/jwks": "0.5.4",
40
+ "@fabrika/auth-core": "0.5.5",
41
+ "@fabrika/jwks": "0.5.5",
42
42
  "jose": "^5.9.0",
43
- "@fabrika/platform": "0.5.4"
43
+ "@fabrika/platform": "0.5.5"
44
44
  }
45
45
  }
package/src/authorize.ts CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  callerAdmittedAt,
27
27
  callerOf,
28
28
  type CompiledEndpointTable,
29
+ FORM_MULTIPART_TYPE,
29
30
  formatEndpointHeader,
30
31
  planEndpoint,
31
32
  satisfies,
@@ -77,7 +78,10 @@ export type DenyReason =
77
78
  | 'cross_site'
78
79
  /** The request's `Content-Type` is not one the endpoint admits. */
79
80
  | 'content_type'
80
- /** A preflight for an endpoint the contract does not open to another origin, or from an origin it does not admit. */
81
+ /**
82
+ * A preflight for an endpoint the contract does not open to another origin, or from an origin it does not admit; or a
83
+ * multipart form's request from such an origin, which a browser sends without a preflight.
84
+ */
81
85
  | 'cross_origin'
82
86
  /** IAM refused the delegation grant a membership or scope-creating endpoint needs. */
83
87
  | 'delegation_refused'
@@ -301,7 +305,8 @@ export class Authorizer {
301
305
  private async decideRow(app: ProxyApp, row: ManifestEndpointRow, request: ForwardedRequest, crossOrigin: string | null): Promise<Decision> {
302
306
  const endpoint = formatEndpointHeader(row.id, row.digest)
303
307
  if (row.requirement === null) {
304
- return admitBody(row, request.headers) ?? { outcome: 'allow', token: null, subject: null, endpoint, delegation: null, crossOrigin }
308
+ return admitBody(row, request.headers) ?? admitFormOrigin(app, row, request, crossOrigin)
309
+ ?? { outcome: 'allow', token: null, subject: null, endpoint, delegation: null, crossOrigin }
305
310
  }
306
311
  const oauth = oauthBindingOf(app, row)
307
312
  if (oauth === 'unregistered') {
@@ -656,6 +661,20 @@ function withBearerChallenge(denial: Denial, oauth: OAuthBinding | null): Denial
656
661
  return { ...denial, challenge: denial.status === 401 ? { ...challenge, error: 'invalid_token' } : challenge }
657
662
  }
658
663
 
664
+ /**
665
+ * A multipart form admits a browser's request only from the application's own origin or one the row admits (ADR-0022
666
+ * "Cross-origin endpoints"). A page on any origin sends `multipart/form-data` without a preflight, so the proxy refuses
667
+ * at the request what it refuses at the preflight of a JSON form. A request without `Origin` is no browser's
668
+ * cross-origin request.
669
+ */
670
+ function admitFormOrigin(app: ProxyApp, row: ManifestEndpointRow, request: ForwardedRequest, crossOrigin: string | null): Denial | null {
671
+ const origin = request.headers.get(ORIGIN_HEADER)
672
+ if (row.kind !== 'form' || origin === null || crossOrigin !== null || !(row.admission?.contentTypes.includes(FORM_MULTIPART_TYPE) ?? false)) {
673
+ return null
674
+ }
675
+ return origin === `${app.scheme}://${request.url.host}` ? null : { outcome: 'deny', status: 403, reason: 'cross_origin' }
676
+ }
677
+
659
678
  /** An endpoint that admits a body admits only its declared media types (ADR-0022 rule 3, admission). */
660
679
  function admitBody(row: ManifestEndpointRow, headers: Headers): Denial | null {
661
680
  if (row.admission === null) {