stitchkit 0.50.0 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +3 -3
- package/dist/{index-jewp9r0a.js → index-8qghvg18.js} +1 -1
- package/dist/{index-h05ygjqx.js → index-bmnhqtya.js} +4 -1
- package/dist/{index-a4qmxybq.js → index-d6jj3cp5.js} +5 -154
- package/dist/index-drsnz8gw.js +249 -0
- package/dist/{index-p9d4cxt5.js → index-gff2mxzk.js} +1 -1
- package/dist/{index-0hj37z43.js → index-h55e1wyq.js} +2 -2
- package/dist/{index-v58mwa19.js → index-psrxjvbw.js} +2 -2
- package/dist/{index-03j2t778.js → index-tw7mhqgy.js} +15 -4
- package/dist/node.d.ts +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +7 -6
- package/dist/observability/index.js +3 -3
- package/dist/server/implement.d.ts +31 -3
- package/dist/server/implement.d.ts.map +1 -1
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +14 -14
- package/dist/server/middleware/auth.d.ts +63 -3
- package/dist/server/middleware/auth.d.ts.map +1 -1
- package/dist/testing.js +2 -2
- package/dist/tools/list-names.d.ts +13 -1
- package/dist/tools/list-names.d.ts.map +1 -1
- package/dist/tools.d.ts +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +13 -8
- package/llms-full.txt +158 -6
- package/package.json +1 -1
- package/dist/index-r1qp4rve.js +0 -57
package/llms-full.txt
CHANGED
|
@@ -737,7 +737,12 @@ Three boundaries worth knowing:
|
|
|
737
737
|
it). It degrades to `unknown`, so it can no longer pose as a `string`; using it
|
|
738
738
|
in a typed position fails.
|
|
739
739
|
- The map states what **your** `beforeHandle` / `createAuthHook.inject` puts on
|
|
740
|
-
the context. The framework does not verify
|
|
740
|
+
the context. The framework does not verify a hand-written claim (→ ADR 0075) —
|
|
741
|
+
so prefer not to write one: scoped auth rules declare their contribution by
|
|
742
|
+
performing it, and `createScopedImplement<AuthScopes<typeof hook>>()` derives
|
|
743
|
+
the map from the hook
|
|
744
|
+
([details](./auth-and-errors.md#scoped-rules--the-map-createscopedimplement-consumes),
|
|
745
|
+
→ ADR 0078).
|
|
741
746
|
- An endpoint hoisted out of the contract literal widens its `scope` to `string`,
|
|
742
747
|
which is no key of any map, and lands in the error branch. An endpoint whose
|
|
743
748
|
`scope` is added by a conditional spread is optional, so it resolves to **both**
|
|
@@ -753,6 +758,14 @@ The same map has a registry form and a streaming form:
|
|
|
753
758
|
const implementAll = createScopedImplementRegistry<Scopes>()
|
|
754
759
|
export const services = implementAll(apiContractRegistry, { posts, users })
|
|
755
760
|
|
|
761
|
+
// a service FILE declares its handlers typed but unbound — binding happens
|
|
762
|
+
// once, in the registry. Curried out of necessity: one call cannot both take
|
|
763
|
+
// the contract and contextually infer the handlers from it.
|
|
764
|
+
export const posts = implementFor.declare(postsContract)({
|
|
765
|
+
list: (ctx) => listPosts(ctx.userId), // ctx typed per endpoint scope
|
|
766
|
+
purge: (ctx) => (ctx.isAdmin ? purge() : []),
|
|
767
|
+
})
|
|
768
|
+
|
|
756
769
|
// a streaming multipart handler that reads its scope's fields. The endpoint
|
|
757
770
|
// must declare its own `scope`, and only that literal is accepted: an endpoint
|
|
758
771
|
// inheriting the contract's scope cannot be verified from here, and guessing it
|
|
@@ -785,9 +798,22 @@ export const apiServices = implementRegistry(apiContractRegistry, {
|
|
|
785
798
|
|
|
786
799
|
Every registry key is required, extra keys fail, and each handler map is checked
|
|
787
800
|
against its own contract. The returned service order follows the contract
|
|
788
|
-
registry order
|
|
801
|
+
registry order — and the same services ride along under `.byKey`, so a consumer
|
|
802
|
+
whose registry keys are load-bearing (filtering a tool surface per caller) never
|
|
803
|
+
rebuilds a prefix lookup by hand:
|
|
804
|
+
|
|
805
|
+
```ts
|
|
806
|
+
const services = implementRegistry(apiContractRegistry, handlers)
|
|
807
|
+
createServer({ services }) // the array, as before
|
|
808
|
+
const visible = [services.byKey.users] // the same objects, by key
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
`createImplementRegistry<AppContext>()` fixes the application
|
|
789
812
|
context once, like `createImplement` does for a single contract. Runtime callers
|
|
790
|
-
also fail first on missing/extra keys and duplicate
|
|
813
|
+
also fail first on missing/extra keys and on a duplicate prefix **within one
|
|
814
|
+
scope** — the same prefix under two different group scopes is legal (their URLs
|
|
815
|
+
are separated by `scopePrefixes`, and a genuine path clash is the router's own
|
|
816
|
+
construction error).
|
|
791
817
|
|
|
792
818
|
The registry is intentionally flat: every key must point to one concrete
|
|
793
819
|
`defineContract()` result. Composed namespace arrays are mounted explicitly with
|
|
@@ -3755,7 +3781,61 @@ endpoints under the group declare `scope: 'tenant'`.
|
|
|
3755
3781
|
| `onForbidden` | thrown when an identity is present but the rule rejects it (default 403) |
|
|
3756
3782
|
|
|
3757
3783
|
Annotate `rules` with `satisfies Record<MyScope, AuthRule<User>>` so the compiler
|
|
3758
|
-
catches a scope you forgot to cover.
|
|
3784
|
+
catches a scope you forgot to cover. With the scoped rule objects below, widen
|
|
3785
|
+
the annotation to
|
|
3786
|
+
`satisfies Record<MyScope, AuthRule<User> | ScopedAuthRule<User, object>>` — it
|
|
3787
|
+
keeps the coverage check and does not disturb the derivation. (When every scope
|
|
3788
|
+
comes from the derived map, the check is already implicit: a scope missing from
|
|
3789
|
+
`rules` is no key of the map, and a contract using it fails to compile.)
|
|
3790
|
+
|
|
3791
|
+
### Scoped rules — the map `createScopedImplement` consumes
|
|
3792
|
+
|
|
3793
|
+
A rule may take an object form, `{ rule, inject }`, where `inject` **returns**
|
|
3794
|
+
the fields this scope contributes. That return type is a declaration the hook
|
|
3795
|
+
derives a scope→context map from — feed it to
|
|
3796
|
+
[`createScopedImplement`](./server.md#per-scope-handler-context--createscopedimplement)
|
|
3797
|
+
and the map can never drift from the hook, because it is computed from it:
|
|
3798
|
+
|
|
3799
|
+
```ts
|
|
3800
|
+
const authHook = createAuthHook({
|
|
3801
|
+
resolve: sessionResolver, // no explicit generic — let it infer
|
|
3802
|
+
rules: {
|
|
3803
|
+
public: { rule: 'public', inject: (user) => ({ userId: user.id }) },
|
|
3804
|
+
user: { rule: 'authenticated', inject: (user) => ({ userId: user.id }) },
|
|
3805
|
+
admin: {
|
|
3806
|
+
rule: (user) => user.admin,
|
|
3807
|
+
inject: (user) => ({ userId: user.id, isAdmin: user.admin }),
|
|
3808
|
+
},
|
|
3809
|
+
},
|
|
3810
|
+
})
|
|
3811
|
+
|
|
3812
|
+
export const implementFor = createScopedImplement<AuthScopes<typeof authHook>>()
|
|
3813
|
+
```
|
|
3814
|
+
|
|
3815
|
+
- A `'public'` rule's fields come out **optional**: public admits the anonymous
|
|
3816
|
+
caller, it does not refuse to know the logged-in one — `resolve` and `inject`
|
|
3817
|
+
still run, so a public `me` / `logout` handler reads `ctx.userId` as
|
|
3818
|
+
`string | undefined` and narrows, instead of being pushed toward a cast by a
|
|
3819
|
+
`public: object` map.
|
|
3820
|
+
- Any other rule's fields are required — the rule rejected the request before
|
|
3821
|
+
the handler if no identity resolved.
|
|
3822
|
+
- A scope with no rule is no key of the derived map, so a contract using it
|
|
3823
|
+
fails to compile — the type-level mirror of the hook's fail-closed
|
|
3824
|
+
`no rule for scope` throw.
|
|
3825
|
+
|
|
3826
|
+
The per-rule `inject` runs after the shared one, only when an identity
|
|
3827
|
+
resolved, and **before** the rule check — it may run for an identity the rule
|
|
3828
|
+
then rejects, so keep it pure and synchronous (an async `inject` is a compile
|
|
3829
|
+
error, and a runtime one for untyped callers: merging a Promise would merge
|
|
3830
|
+
nothing). Write rule objects inline in `rules` — a hoisted, annotated rule
|
|
3831
|
+
widens its inject and degrades that scope's fields to `object`.
|
|
3832
|
+
|
|
3833
|
+
Derivation needs the identity generic to be **inferred** (write no explicit
|
|
3834
|
+
`createAuthHook<User>` — TypeScript has no partial inference); with an explicit
|
|
3835
|
+
generic, or fields injected outside the hook, keep the hand-written map. Two
|
|
3836
|
+
hooks guarding different scopes compose by intersection:
|
|
3837
|
+
`createScopedImplement<AuthScopes<typeof userHook> & AuthScopes<typeof botHook>>()`.
|
|
3838
|
+
→ ADR 0078
|
|
3759
3839
|
|
|
3760
3840
|
### Auth on the tool surface — `resolveFromContext`
|
|
3761
3841
|
|
|
@@ -4797,6 +4877,71 @@ closing one whose chain is already running leaves that chain to finish and keeps
|
|
|
4797
4877
|
the handle claimed, since a second binding could not force it. `promise` is
|
|
4798
4878
|
already observed internally, so ignoring it never raises an `unhandledRejection`.
|
|
4799
4879
|
|
|
4880
|
+
#### Composite shutdown target — parallel domain drains
|
|
4881
|
+
|
|
4882
|
+
`bindProcessSignals` takes `Pick<ManagedServerHandle, 'shutdown'>` — an
|
|
4883
|
+
**interface**, not the server. An application whose shutdown spans several
|
|
4884
|
+
domains (transport, bots, agent runs, broadcasts) composes them into one target;
|
|
4885
|
+
the signal machine stays the framework's, the composition stays yours:
|
|
4886
|
+
|
|
4887
|
+
```ts
|
|
4888
|
+
const composite: ShutdownTarget = {
|
|
4889
|
+
async shutdown(options?: ShutdownOptions): Promise<ShutdownResult> {
|
|
4890
|
+
const { signal } = ShutdownOptionsSchema.parse(options ?? {})
|
|
4891
|
+
// Domain drains run in parallel with the transport drain, all watching the
|
|
4892
|
+
// same force signal the second OS signal aborts.
|
|
4893
|
+
const [transport, bots, agents, broadcasts] = await Promise.all([
|
|
4894
|
+
server.shutdown(options),
|
|
4895
|
+
drainBots(signal),
|
|
4896
|
+
drainAgents(signal),
|
|
4897
|
+
drainBroadcasts(signal),
|
|
4898
|
+
])
|
|
4899
|
+
return {
|
|
4900
|
+
...transport,
|
|
4901
|
+
// Fold domain leftovers into the transport result so `onComplete` sees
|
|
4902
|
+
// one honest outcome.
|
|
4903
|
+
outcome: transport.outcome === 'clean' && bots + agents + broadcasts === 0
|
|
4904
|
+
? 'clean'
|
|
4905
|
+
: 'forced',
|
|
4906
|
+
}
|
|
4907
|
+
},
|
|
4908
|
+
}
|
|
4909
|
+
|
|
4910
|
+
bindProcessSignals(composite, { shutdown: { gracePeriodMs: 30_000 } })
|
|
4911
|
+
```
|
|
4912
|
+
|
|
4913
|
+
The transport half inside stays the real managed handle — its admission gate and
|
|
4914
|
+
HTTP drain are not reimplemented, only composed. Domain drains must watch the
|
|
4915
|
+
`signal` themselves: it is the only channel a second OS signal has into a
|
|
4916
|
+
running chain.
|
|
4917
|
+
|
|
4918
|
+
#### The last line of defence is yours
|
|
4919
|
+
|
|
4920
|
+
The framework never calls `process.exit`, and `process.exitCode` only takes
|
|
4921
|
+
effect once the event loop empties — which is exactly what a stuck resource
|
|
4922
|
+
prevents. If the deployment must exit before the supervisor's `SIGKILL`, arm an
|
|
4923
|
+
application-side timer:
|
|
4924
|
+
|
|
4925
|
+
```ts
|
|
4926
|
+
onShutdown: () => {
|
|
4927
|
+
setTimeout(() => process.exit(1), 60_000).unref()
|
|
4928
|
+
},
|
|
4929
|
+
```
|
|
4930
|
+
|
|
4931
|
+
`unref()` keeps the timer from holding a healthy process open; it only fires if
|
|
4932
|
+
something else already is.
|
|
4933
|
+
|
|
4934
|
+
#### When NOT to use `bindProcessSignals`
|
|
4935
|
+
|
|
4936
|
+
- The application already runs a signal machine with states this one does not
|
|
4937
|
+
have (staged escalation policies, per-signal semantics beyond
|
|
4938
|
+
first/force/escalate). Two machines on the same signals is worse than either.
|
|
4939
|
+
- Signal policy must differ per signal (e.g. `SIGHUP` = reload, not shutdown) —
|
|
4940
|
+
bind only the terminating signals here and keep the rest yours.
|
|
4941
|
+
- Something else in the process must keep listening for the same signal: the
|
|
4942
|
+
escalation path restores the default disposition only when nothing else
|
|
4943
|
+
listens, and reports `onEscalationBlocked` otherwise.
|
|
4944
|
+
|
|
4800
4945
|
The server owns HTTP/Socket.IO transport resources. MCP, databases, queues and
|
|
4801
4946
|
domain run-state remain application resources and close explicitly after server
|
|
4802
4947
|
drain. Do not call `runtime.stop()` or `socket.io.close()` in parallel with
|
|
@@ -6217,7 +6362,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
|
|
|
6217
6362
|
| `createHandler` | function | the router as a bare `(req) => Response` — [guide](../guide/server.md#createserver) |
|
|
6218
6363
|
| `implement` | function | bind a contract to typed handlers — [guide](../guide/server.md#implement) |
|
|
6219
6364
|
| `createImplement` | function | fix the handler context type once |
|
|
6220
|
-
| `createScopedImplement` | function | fix one scope→context map, then type every handler by its endpoint's effective scope — [guide](../guide/server.md#per-scope-handler-context--createscopedimplement) |
|
|
6365
|
+
| `createScopedImplement` | function | fix one scope→context map, then type every handler by its endpoint's effective scope; `.declare(contract)(handlers)` types without binding for the registry path, `.stream(scope, …)` covers streaming multipart — [guide](../guide/server.md#per-scope-handler-context--createscopedimplement) |
|
|
6221
6366
|
| `ScopedHandlers` | _type_ | handler map typed per endpoint by its effective scope |
|
|
6222
6367
|
| `ScopeContexts` | _type_ | scope → extra context fields map accepted by `createScopedImplement` |
|
|
6223
6368
|
| `EffectiveScope` | _type_ | an endpoint's own `scope`, else its contract's group scope |
|
|
@@ -6229,6 +6374,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
|
|
|
6229
6374
|
| `implementRegistry` | function | bind one exact contract registry to one exact backend handler registry |
|
|
6230
6375
|
| `createImplementRegistry` | function | context-typed factory for `implementRegistry` |
|
|
6231
6376
|
| `ImplementationRegistry` | _type_ | flat literal registry of concrete contracts accepted by `implementRegistry` |
|
|
6377
|
+
| `KeyedServices` | _type_ | registry result: the mount-ordered `ServiceDef[]` carrying the same services under `.byKey` |
|
|
6232
6378
|
| `RegistryHandlers` | _type_ | exact backend handler registry inferred from a contract registry |
|
|
6233
6379
|
| `ExactRegistryHandlers` | _type_ | fail-first handler shape that rejects extra registry and endpoint keys |
|
|
6234
6380
|
| `staticRoute` | function | a raw route that serves a directory |
|
|
@@ -6290,6 +6436,11 @@ Also re-exports the error helpers from `stitchkit/contract`.
|
|
|
6290
6436
|
| `AuthHook` | _type_ | the hook `createAuthHook` returns |
|
|
6291
6437
|
| `AuthHookConfig` | _type_ | config for `createAuthHook` |
|
|
6292
6438
|
| `AuthRule` | _type_ | `'public' \| 'authenticated' \| predicate` |
|
|
6439
|
+
| `ScopedAuthRule` | _type_ | a rule plus its typed context contribution — `{ rule, inject? }` |
|
|
6440
|
+
| `AuthRules` | _type_ | the `rules` map: bare rules or scoped rules |
|
|
6441
|
+
| `RuleScopes` | _type_ | scope→context map derived from a `rules` object; `'public'` fields become optional |
|
|
6442
|
+
| `ScopedAuthHook` | _type_ | an auth hook carrying its derived scope map at the type level |
|
|
6443
|
+
| `AuthScopes` | _type_ | recover the derived map — `createScopedImplement<AuthScopes<typeof hook>>()` |
|
|
6293
6444
|
| `BearerResolverConfig` | _type_ | config for `createBearerResolver` |
|
|
6294
6445
|
| `JwtPayload` | _type_ | a decoded JWT payload |
|
|
6295
6446
|
| `SignJwtOptions` | _type_ | options for `signJwt` (expiry, claims) |
|
|
@@ -6352,7 +6503,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
|
|
|
6352
6503
|
| `ProcessSignalsErrorPhase` | _type_ | `'prepare' \| 'shutdown' \| 'complete'` — which phase an `onError` report came from |
|
|
6353
6504
|
| `SignalSource` | _type_ | injectable signal source — `process` by default |
|
|
6354
6505
|
| `ProcessSignalName` | _type_ | signal names the binding accepts (owned, so the published types need no `@types/node`) |
|
|
6355
|
-
| `ShutdownTarget` | _type_ | the `shutdown`-only slice
|
|
6506
|
+
| `ShutdownTarget` | _type_ | the `shutdown`-only slice a binding needs — an interface, so a [composite target](../guide/testing-and-deployment.md#composite-shutdown-target--parallel-domain-drains) can drain several domains under one signal machine |
|
|
6356
6507
|
| `createRateLimiter` | function | token-bucket rate limiting — [guide](../guide/server.md#rate-limiting) |
|
|
6357
6508
|
| `createCache` | function | an in-memory TTL cache |
|
|
6358
6509
|
| `CacheOptions` | _type_ | bounded-cache options, including the maximum retained entry count |
|
|
@@ -6481,6 +6632,7 @@ payload.
|
|
|
6481
6632
|
| `resolveMedia` | function | resolve a media reference for a tool result |
|
|
6482
6633
|
| `validateMcpSchemas` | function | object-shaped assertion over the exact advertised schema surface — compatibility, typed properties and portable formats ([guide](../guide/mcp-and-agents.md#mcp-schema-validation-profile)) |
|
|
6483
6634
|
| `listToolNames` | function | every contract/runtime tool name with origin, identity and transports — for stable snapshots — [guide](../guide/mcp-and-agents.md#pinning-tool-names--listtoolnames) |
|
|
6635
|
+
| `listContractToolNames` | function | the same listing straight from contracts — no handlers or stub services needed |
|
|
6484
6636
|
| `McpHandlerConfig` | _type_ | server surface plus stateless HTTP transport config |
|
|
6485
6637
|
| `McpHttpConfig` | _type_ | HTTP auth, protected-resource, legacy-era and security options |
|
|
6486
6638
|
| `McpHttpHandler` | _type_ | framework-owned `{ fetch(request), close() }` lifecycle |
|
package/package.json
CHANGED
package/dist/index-r1qp4rve.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
// src/server/middleware/cors.ts
|
|
2
|
-
var DEFAULT_CORS_ALLOW_HEADERS = "Content-Type, Authorization, X-Trace-Id, traceparent, tracestate";
|
|
3
|
-
var DEFAULT_CORS_EXPOSE_HEADERS = "Content-Disposition, Content-Length, Content-Range, Accept-Ranges, ETag, Last-Modified, X-Request-Id";
|
|
4
|
-
function assertCorsConfig(config) {
|
|
5
|
-
if (config.credentials && (config.origin === undefined || config.origin === "*")) {
|
|
6
|
-
throw new Error("[stitchkit] cors: `credentials: true` cannot be combined with a wildcard origin. " + "Set `origin` to an explicit string or list.");
|
|
7
|
-
}
|
|
8
|
-
if (config.origin === undefined) {
|
|
9
|
-
throw new Error("[stitchkit] cors: `origin` is required. Pass an explicit origin (or list), or '*' to deliberately allow every origin — omit `cors` entirely to emit no CORS headers.");
|
|
10
|
-
}
|
|
11
|
-
if (Array.isArray(config.origin) && config.origin.length === 0) {
|
|
12
|
-
throw new Error("[stitchkit] cors: `origin` cannot be an empty list. Pass explicit origins, or '*' to deliberately allow every origin.");
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
function resolveOrigin(config, requestOrigin) {
|
|
16
|
-
if (config.origin === undefined)
|
|
17
|
-
return;
|
|
18
|
-
if (config.origin === "*") {
|
|
19
|
-
return "*";
|
|
20
|
-
}
|
|
21
|
-
if (Array.isArray(config.origin)) {
|
|
22
|
-
if (!requestOrigin)
|
|
23
|
-
return;
|
|
24
|
-
const lower = requestOrigin.toLowerCase();
|
|
25
|
-
return config.origin.some((o) => o.toLowerCase() === lower) ? requestOrigin : undefined;
|
|
26
|
-
}
|
|
27
|
-
return config.origin;
|
|
28
|
-
}
|
|
29
|
-
function corsHeaders(config, requestOrigin) {
|
|
30
|
-
const allowOrigin = resolveOrigin(config, requestOrigin);
|
|
31
|
-
const headers = {
|
|
32
|
-
"Access-Control-Allow-Methods": config.methods ?? "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
33
|
-
"Access-Control-Allow-Headers": config.headers ?? DEFAULT_CORS_ALLOW_HEADERS
|
|
34
|
-
};
|
|
35
|
-
const expose = Array.isArray(config.exposeHeaders) ? config.exposeHeaders.join(", ") : config.exposeHeaders ?? DEFAULT_CORS_EXPOSE_HEADERS;
|
|
36
|
-
if (expose !== "") {
|
|
37
|
-
headers["Access-Control-Expose-Headers"] = expose;
|
|
38
|
-
}
|
|
39
|
-
if (allowOrigin !== undefined) {
|
|
40
|
-
headers["Access-Control-Allow-Origin"] = allowOrigin;
|
|
41
|
-
if (config.credentials) {
|
|
42
|
-
headers["Access-Control-Allow-Credentials"] = "true";
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
if (Array.isArray(config.origin)) {
|
|
46
|
-
headers.Vary = "Origin";
|
|
47
|
-
}
|
|
48
|
-
return headers;
|
|
49
|
-
}
|
|
50
|
-
function corsPreflightResponse(config, req) {
|
|
51
|
-
return new Response(null, {
|
|
52
|
-
status: 204,
|
|
53
|
-
headers: corsHeaders(config, req.headers.get("origin"))
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export { DEFAULT_CORS_ALLOW_HEADERS, DEFAULT_CORS_EXPOSE_HEADERS, assertCorsConfig, corsHeaders, corsPreflightResponse };
|