@haddowg/json-api-client 1.2.0 → 1.3.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.
package/dist/ajv.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as Validator, n as SchemaMap } from "./validate-DgCUbzgT.cjs";
1
+ import { a as Validator, n as SchemaMap } from "./validate-B98ghVgw.cjs";
2
2
  import { ValidateFunction } from "ajv";
3
3
 
4
4
  //#region src/ajv.d.ts
package/dist/ajv.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as Validator, n as SchemaMap } from "./validate-DgCUbzgT.mjs";
1
+ import { a as Validator, n as SchemaMap } from "./validate-B98ghVgw.mjs";
2
2
  import { ValidateFunction } from "ajv";
3
3
 
4
4
  //#region src/ajv.d.ts
package/dist/index.cjs CHANGED
@@ -540,12 +540,31 @@ const ATOMIC_MEDIA_TYPE = mediaType({ ext: [ATOMIC_EXT] });
540
540
  //#region src/request.ts
541
541
  const isAbsolute = (path) => /^https?:\/\//i.test(path);
542
542
  const RELATIVE_ORIGIN = "http://relative.invalid";
543
- /** Resolve a server link against `baseUrl` per RFC 3986. An absolute link is kept verbatim. */
543
+ /**
544
+ * Resolve a server link against `baseUrl` per RFC 3986 and check where it lands. A link on the
545
+ * base's origin is followed; a relative base (e.g. `/api`) takes the page's origin in a browser and
546
+ * otherwise only accepts a link that keeps the host. Anything that isn't plain http(s) is refused.
547
+ */
544
548
  function resolveLink(link, baseUrl) {
545
- if (isAbsolute(link)) return link;
546
- if (isAbsolute(baseUrl)) return new URL(link, baseUrl).href;
547
- const resolved = new URL(link, new URL(baseUrl, RELATIVE_ORIGIN));
548
- return resolved.origin === RELATIVE_ORIGIN ? resolved.pathname + resolved.search + resolved.hash : resolved.href;
549
+ let normalised = link.replace(/[\t\n\r]/g, "");
550
+ let start = 0;
551
+ while (start < normalised.length && normalised.charCodeAt(start) <= 32) start++;
552
+ normalised = normalised.slice(start);
553
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(normalised)?.[1]?.toLowerCase();
554
+ if (scheme !== void 0 && scheme !== "http" && scheme !== "https") throw new Error(`Refusing to follow link "${link}": only http(s) links are followed`);
555
+ const relativeBase = !isAbsolute(baseUrl);
556
+ const page = globalThis.location?.href;
557
+ const base = new URL(baseUrl, relativeBase ? typeof page === "string" ? page : RELATIVE_ORIGIN : void 0);
558
+ const resolved = new URL(link, base);
559
+ const sameOrigin = resolved.origin === base.origin && (scheme === void 0 || /^https?:\/\//i.test(normalised));
560
+ if (!sameOrigin) return {
561
+ url: resolved.href,
562
+ sameOrigin
563
+ };
564
+ return {
565
+ url: relativeBase ? resolved.pathname + resolved.search + resolved.hash : resolved.href,
566
+ sameOrigin
567
+ };
549
568
  }
550
569
  const append = (parts, key, value) => {
551
570
  if (value === void 0 || value === null || value === "") return;
@@ -583,14 +602,21 @@ function serializeQuery(query) {
583
602
  return parts.join("&");
584
603
  }
585
604
  /**
586
- * Execute a request: build the URL (a server link resolved against `baseUrl`, an absolute path
587
- * verbatim, else `baseUrl + path`), apply content negotiation and any async per-request headers,
605
+ * Execute a request: build the URL (a server link resolved against `baseUrl` and held to its
606
+ * origin, an absolute path verbatim, else `baseUrl + path`), apply content negotiation and any async per-request headers,
588
607
  * then drive the transport. A 2xx with a body parses to a {@link Document}; an empty /
589
608
  * 204 body resolves to `undefined`. A non-2xx throws a {@link JsonApiError} carrying the
590
609
  * parsed error objects (tolerating a non-JSON error body with an empty list).
591
610
  */
592
611
  async function execute(ctx, req) {
593
- let url = req.link ? resolveLink(req.path, ctx.baseUrl) : isAbsolute(req.path) ? req.path : ctx.baseUrl + req.path;
612
+ let url;
613
+ let sendHeaders = true;
614
+ if (req.link) {
615
+ const link = resolveLink(req.path, ctx.baseUrl);
616
+ if (!link.sameOrigin && ctx.allowCrossOriginLinks !== true) throw new Error(`Refusing to follow link "${req.path}": it leaves the base URL's origin (set allowCrossOriginLinks to follow it without the configured headers)`);
617
+ url = link.url;
618
+ sendHeaders = link.sameOrigin;
619
+ } else url = isAbsolute(req.path) ? req.path : ctx.baseUrl + req.path;
594
620
  if (req.query) {
595
621
  const qs = serializeQuery(req.query);
596
622
  if (qs) url += (url.includes("?") ? "&" : "?") + qs;
@@ -610,7 +636,7 @@ async function execute(ctx, req) {
610
636
  headers["Content-Type"] = req.contentType ?? negotiated;
611
637
  transportReq.body = req.raw && typeof req.body === "string" ? req.body : JSON.stringify(req.body);
612
638
  }
613
- if (ctx.headers) mergeHeaders(headers, await ctx.headers());
639
+ if (ctx.headers && sendHeaders) mergeHeaders(headers, await ctx.headers());
614
640
  const res = await ctx.transport(transportReq);
615
641
  if (res.status >= 200 && res.status < 300) return parseDocument(res.body);
616
642
  throw new JsonApiError(res.status, parseErrors(res.body));
@@ -1020,6 +1046,18 @@ const fetchTransport = async (req) => {
1020
1046
  /** Substitute `{key}` placeholders in a path template (`/albums/{id}/{rel}` -> `/albums/1/tracks`). */
1021
1047
  const fill = (template, vars) => template.replace(/\{(\w+)\}/g, (_, key) => vars[key] ?? `{${key}}`);
1022
1048
  /**
1049
+ * Fill a type's path template, putting a relation's URI segment (its `uriField`, else its name)
1050
+ * in place of `{rel}`. `vars.rel` stays the relation's name everywhere else.
1051
+ */
1052
+ function fillPath(ctx, type, template, vars) {
1053
+ const rel = vars["rel"];
1054
+ const uriField = rel === void 0 ? void 0 : ctx.descriptor[type]?.relations[rel]?.uriField;
1055
+ return fill(template, uriField === void 0 ? vars : {
1056
+ ...vars,
1057
+ rel: uriField
1058
+ });
1059
+ }
1060
+ /**
1023
1061
  * Run an operation: resolve the path template from the type's descriptor, fill its
1024
1062
  * placeholders, execute, and materialise the resulting document. A no-body / 204 response
1025
1063
  * materialises to `undefined`; an operation the type does not declare (no path template) throws
@@ -1032,7 +1070,7 @@ async function run(ctx, type, operation, vars, query, linkage = false, relationF
1032
1070
  if (template === void 0) throw new Error(`No "${operation}" path declared for type "${type}"`);
1033
1071
  const req = {
1034
1072
  method: "GET",
1035
- path: fill(template, vars)
1073
+ path: fillPath(ctx, type, template, vars)
1036
1074
  };
1037
1075
  if (query !== void 0) {
1038
1076
  const { signal, ...params } = query;
@@ -1082,7 +1120,7 @@ async function runWrite(ctx, spec) {
1082
1120
  if (template === void 0) throw new Error(`No "${spec.operation}" path declared for type "${spec.type}"`);
1083
1121
  const req = {
1084
1122
  method: spec.method,
1085
- path: fill(template, spec.vars),
1123
+ path: fillPath(ctx, spec.type, template, spec.vars),
1086
1124
  body: spec.body
1087
1125
  };
1088
1126
  if (spec.query !== void 0) req.query = spec.query;
@@ -1181,8 +1219,9 @@ function relationshipAccessor(ctx, type, id, rel) {
1181
1219
  return mutations === void 0 ? true : mutations[verb] === true;
1182
1220
  };
1183
1221
  const accessor = {};
1184
- if (relation?.relationship !== false) accessor["get"] = (query) => run(ctx, type, "fetchRelationship", vars, query, true, rel);
1185
- if (relation?.related !== false) accessor["related"] = (query) => run(ctx, type, "fetchRelated", vars, query, false, rel);
1222
+ const readable = relation?.writeOnly !== true;
1223
+ if (readable && relation?.relationship !== false) accessor["get"] = (query) => run(ctx, type, "fetchRelationship", vars, query, true, rel);
1224
+ if (readable && relation?.related !== false) accessor["related"] = (query) => run(ctx, type, "fetchRelated", vars, query, false, rel);
1186
1225
  if (cardinality === "many") {
1187
1226
  if (advertises("add")) accessor["add"] = (refs) => mutate("POST", refs);
1188
1227
  if (advertises("remove")) accessor["remove"] = (refs) => mutate("DELETE", refs);
@@ -1267,7 +1306,8 @@ function createClient(descriptor, options) {
1267
1306
  const request = {
1268
1307
  baseUrl: options.baseUrl,
1269
1308
  transport: options.transport ?? fetchTransport,
1270
- ...options.headers !== void 0 ? { headers: options.headers } : {}
1309
+ ...options.headers !== void 0 ? { headers: options.headers } : {},
1310
+ ...options.allowCrossOriginLinks !== void 0 ? { allowCrossOriginLinks: options.allowCrossOriginLinks } : {}
1271
1311
  };
1272
1312
  const validator = resolveValidator(options.validate);
1273
1313
  const ctx = {
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as ResourceAccessors, $t as ActionInput, A as DefaultActionTypes, At as Edge, B as IdentifierMember, Bt as CANONICAL_PAGE_KEYS, C as fetchTransport, Ct as JSON_API_MEDIA_TYPE, D as Collection, Dt as mediaType, E as Client, Et as ResourceIdentifier, F as FieldSelectionMap, Ft as ResourceEnvelope, G as MemberName, Gt as PageMeta, H as IncludedRelations, Ht as Page, I as FieldsMap, It as ResourceLinks, J as RelationName, Jt as PageParamsOf, K as PivotInput, Kt as PageOf, L as FilterKey, Lt as materialise, M as DefaultWriteAttributes, Mt as MaterialiseContext, N as EdgeMembers, Nt as NavigateOptions, O as CountToken, Ot as ArrayEnvelope, P as ErrorCodeOf, Pt as RelationView, Q as RelationshipAccessor, Qt as ActionDescriptor, R as FilterQuery, Rt as relatedPaginator, S as TransportResponse, St as ATOMIC_MEDIA_TYPE, T as AtomicResultOf, Tt as MediaTypeParams, U as LinkageRef, Ut as PageKeys, V as IncludePath, Vt as ExactPageParams, W as ListPageParams, Wt as PageLinks, X as RelationPaginator, Xt as TypedPage, Y as RelationPageParams, Yt as ResolvedPaginator, Z as RelationReadQuery, Zt as resolvePaginator, _ as HANDLE_RESERVED, _t as AtomicResults, a as Validator, an as ClientIdPolicy, at as TypeAccessor, b as JsonApiTransport, bt as runAtomic, c as resolveValidator, cn as PaginatorDescriptor, ct as UpdateInput, d as JsonApiContext, dn as PaginatorStrategy, dt as AtomicCreateInput, en as ActionOutput, et as ResourceHandle, f as JsonApiRequest, fn as RelationDescriptor, ft as AtomicDeleteHandle, g as ClientOptions, gt as AtomicResult, h as serializeQuery, hn as StrategyDescriptor, ht as AtomicRef, i as ValidationOption, in as Cardinality, it as SortToken, j as DefaultAttributes, jt as EdgeEnvelope, k as CreateInput, kt as DocumentEnvelope, l as validateDocument, ln as PaginatorKind, lt as WriteOptions, m as execute, mn as ResourceDescriptor, mt as AtomicRecorder, n as SchemaMap, nn as ApiDescriptor, nt as SingleReadQuery, o as WireResource, on as CountableDescriptor, ot as TypeName, p as ReadQuery, pn as RelationMutations, pt as AtomicHandle, q as ReadResult, qt as PageParams, r as ValidationConfig, rn as AtomicDescriptor, rt as SortQuery, s as assertJsonApiDocument, sn as MenuDescriptor, st as TypedReadQuery, t as ResolvedValidator, tn as ActionScope, tt as ResourceObjectView, u as Document, un as PaginatorRole, ut as AtomicCreateHandle, v as HeadersInit, vt as AtomicUpdateHandle, w as ActionsAccessor, wt as LocalIdentifier, x as TransportRequest, xt as ATOMIC_EXT, y as createClient, yt as AtomicUpdateInput, z as HydratedMember, zt as relatedPaginatorKind } from "./validate-DgCUbzgT.cjs";
1
+ import { $ as RelationshipAccessor, $t as ActionDescriptor, A as DefaultActionTypes, At as DocumentEnvelope, B as IdentifierMember, Bt as relatedPaginatorKind, C as fetchTransport, Ct as ATOMIC_MEDIA_TYPE, D as Collection, Dt as ResourceIdentifier, E as Client, Et as MediaTypeParams, F as FieldSelectionMap, Ft as RelationView, G as MemberName, Gt as PageLinks, H as IncludedRelations, Ht as ExactPageParams, I as FieldsMap, It as ResourceEnvelope, J as ReadableRelationName, Jt as PageParams, K as PivotInput, Kt as PageMeta, L as FilterKey, Lt as ResourceLinks, M as DefaultWriteAttributes, Mt as EdgeEnvelope, N as EdgeMembers, Nt as MaterialiseContext, O as CountToken, Ot as mediaType, P as ErrorCodeOf, Pt as NavigateOptions, Q as RelationReadQuery, Qt as resolvePaginator, R as FilterQuery, Rt as materialise, S as TransportResponse, St as ATOMIC_EXT, T as AtomicResultOf, Tt as LocalIdentifier, U as LinkageRef, Ut as Page, V as IncludePath, Vt as CANONICAL_PAGE_KEYS, W as ListPageParams, Wt as PageKeys, X as RelationPageParams, Xt as ResolvedPaginator, Y as RelationName, Yt as PageParamsOf, Z as RelationPaginator, Zt as TypedPage, _ as HANDLE_RESERVED, _n as StrategyDescriptor, _t as AtomicResult, a as Validator, an as Cardinality, at as SortToken, b as JsonApiTransport, bt as AtomicUpdateInput, c as resolveValidator, cn as MenuDescriptor, ct as TypedReadQuery, d as JsonApiContext, dn as PaginatorRole, dt as AtomicCreateHandle, en as ActionInput, et as ResourceAccessors, f as JsonApiRequest, fn as PaginatorStrategy, ft as AtomicCreateInput, g as ClientOptions, gn as ResourceDescriptor, gt as AtomicRef, h as serializeQuery, hn as RelationMutations, ht as AtomicRecorder, i as ValidationOption, in as AtomicDescriptor, it as SortQuery, j as DefaultAttributes, jt as Edge, k as CreateInput, kt as ArrayEnvelope, l as validateDocument, ln as PaginatorDescriptor, lt as UpdateInput, m as execute, mn as RelationDescriptor, mt as AtomicHandle, n as SchemaMap, nn as ActionScope, nt as ResourceObjectView, o as WireResource, on as ClientIdPolicy, ot as TypeAccessor, p as ReadQuery, pn as PivotDescriptor, pt as AtomicDeleteHandle, q as ReadResult, qt as PageOf, r as ValidationConfig, rn as ApiDescriptor, rt as SingleReadQuery, s as assertJsonApiDocument, sn as CountableDescriptor, st as TypeName, t as ResolvedValidator, tn as ActionOutput, tt as ResourceHandle, u as Document, un as PaginatorKind, ut as WriteOptions, v as HeadersInit, vt as AtomicResults, w as ActionsAccessor, wt as JSON_API_MEDIA_TYPE, x as TransportRequest, xt as runAtomic, y as createClient, yt as AtomicUpdateHandle, z as HydratedMember, zt as relatedPaginator } from "./validate-B98ghVgw.cjs";
2
2
 
3
3
  //#region src/errors.d.ts
4
4
  /**
@@ -216,5 +216,5 @@ declare function withRemappedRelationshipPaths(error: JsonApiError, rel: string,
216
216
  */
217
217
  declare function withRemappedAtomicPaths(error: JsonApiError, descriptor: ApiDescriptor, typeForOp: (opIndex: number) => string | undefined): JsonApiError;
218
218
  //#endregion
219
- export { ATOMIC_EXT, ATOMIC_MEDIA_TYPE, ActionDescriptor, ActionInput, ActionOutput, ActionScope, ActionsAccessor, ApiDescriptor, ArrayEnvelope, AtomicCreateHandle, AtomicCreateInput, AtomicDeleteHandle, AtomicDescriptor, AtomicHandle, AtomicRecorder, AtomicRef, AtomicResult, AtomicResultOf, AtomicResults, AtomicUpdateHandle, AtomicUpdateInput, CANONICAL_PAGE_KEYS, Cardinality, Client, ClientIdPolicy, ClientOptions, Collection, CountToken, CountableDescriptor, CreateInput, DefaultActionTypes, DefaultAttributes, DefaultWriteAttributes, Document, DocumentEnvelope, Edge, EdgeEnvelope, EdgeMembers, ErrorCodeGuard, ErrorCodeOf, ExactPageParams, FieldSelectionMap, FieldsMap, FilterKey, FilterQuery, HANDLE_RESERVED, HeadersInit, HydratedMember, IdentifierMember, IncludePath, IncludedRelations, JSON_API_MEDIA_TYPE, JsonApiContext, JsonApiError, JsonApiErrorObject, JsonApiRequest, JsonApiTransport, LinkageRef, ListPageParams, LocalIdentifier, MaterialiseContext, MediaTypeParams, MemberName, MenuDescriptor, NavigateOptions, Page, PageKeys, PageLinks, PageMeta, PageOf, PageParams, PageParamsOf, PaginatorDescriptor, PaginatorKind, PaginatorRole, PaginatorStrategy, PivotInput, ReadQuery, ReadResult, RelationDescriptor, RelationMutations, RelationName, RelationPageParams, RelationPaginator, RelationReadQuery, RelationView, RelationshipAccessor, RelationshipDocument, ResolvedPaginator, ResolvedValidator, ResourceAccessors, ResourceDescriptor, ResourceEnvelope, ResourceHandle, ResourceIdentifier, ResourceLinks, ResourceObjectView, SchemaMap, SingleReadQuery, SortQuery, SortToken, StrategyDescriptor, StructuralGuardError, ToDocumentOptions, TransportRequest, TransportResponse, TypeAccessor, TypeName, TypedPage, TypedReadQuery, UpdateInput, ValidationConfig, ValidationOption, Validator, WireResource, WriteDocument, WriteOptions, assertJsonApiDocument, createClient, execute, fetchTransport, hasErrorCode, materialise, mediaType, relatedPaginator, relatedPaginatorKind, remapPointer, remapRelationshipPointer, resolvePaginator, resolveValidator, runAtomic, serializeQuery, toDocument, toRelationshipDocument, validateDocument, withRemappedAtomicPaths, withRemappedPaths, withRemappedRelationshipPaths };
219
+ export { ATOMIC_EXT, ATOMIC_MEDIA_TYPE, ActionDescriptor, ActionInput, ActionOutput, ActionScope, ActionsAccessor, ApiDescriptor, ArrayEnvelope, AtomicCreateHandle, AtomicCreateInput, AtomicDeleteHandle, AtomicDescriptor, AtomicHandle, AtomicRecorder, AtomicRef, AtomicResult, AtomicResultOf, AtomicResults, AtomicUpdateHandle, AtomicUpdateInput, CANONICAL_PAGE_KEYS, Cardinality, Client, ClientIdPolicy, ClientOptions, Collection, CountToken, CountableDescriptor, CreateInput, DefaultActionTypes, DefaultAttributes, DefaultWriteAttributes, Document, DocumentEnvelope, Edge, EdgeEnvelope, EdgeMembers, ErrorCodeGuard, ErrorCodeOf, ExactPageParams, FieldSelectionMap, FieldsMap, FilterKey, FilterQuery, HANDLE_RESERVED, HeadersInit, HydratedMember, IdentifierMember, IncludePath, IncludedRelations, JSON_API_MEDIA_TYPE, JsonApiContext, JsonApiError, JsonApiErrorObject, JsonApiRequest, JsonApiTransport, LinkageRef, ListPageParams, LocalIdentifier, MaterialiseContext, MediaTypeParams, MemberName, MenuDescriptor, NavigateOptions, Page, PageKeys, PageLinks, PageMeta, PageOf, PageParams, PageParamsOf, PaginatorDescriptor, PaginatorKind, PaginatorRole, PaginatorStrategy, PivotDescriptor, PivotInput, ReadQuery, ReadResult, ReadableRelationName, RelationDescriptor, RelationMutations, RelationName, RelationPageParams, RelationPaginator, RelationReadQuery, RelationView, RelationshipAccessor, RelationshipDocument, ResolvedPaginator, ResolvedValidator, ResourceAccessors, ResourceDescriptor, ResourceEnvelope, ResourceHandle, ResourceIdentifier, ResourceLinks, ResourceObjectView, SchemaMap, SingleReadQuery, SortQuery, SortToken, StrategyDescriptor, StructuralGuardError, ToDocumentOptions, TransportRequest, TransportResponse, TypeAccessor, TypeName, TypedPage, TypedReadQuery, UpdateInput, ValidationConfig, ValidationOption, Validator, WireResource, WriteDocument, WriteOptions, assertJsonApiDocument, createClient, execute, fetchTransport, hasErrorCode, materialise, mediaType, relatedPaginator, relatedPaginatorKind, remapPointer, remapRelationshipPointer, resolvePaginator, resolveValidator, runAtomic, serializeQuery, toDocument, toRelationshipDocument, validateDocument, withRemappedAtomicPaths, withRemappedPaths, withRemappedRelationshipPaths };
220
220
  //# sourceMappingURL=index.d.cts.map
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as ResourceAccessors, $t as ActionInput, A as DefaultActionTypes, At as Edge, B as IdentifierMember, Bt as CANONICAL_PAGE_KEYS, C as fetchTransport, Ct as JSON_API_MEDIA_TYPE, D as Collection, Dt as mediaType, E as Client, Et as ResourceIdentifier, F as FieldSelectionMap, Ft as ResourceEnvelope, G as MemberName, Gt as PageMeta, H as IncludedRelations, Ht as Page, I as FieldsMap, It as ResourceLinks, J as RelationName, Jt as PageParamsOf, K as PivotInput, Kt as PageOf, L as FilterKey, Lt as materialise, M as DefaultWriteAttributes, Mt as MaterialiseContext, N as EdgeMembers, Nt as NavigateOptions, O as CountToken, Ot as ArrayEnvelope, P as ErrorCodeOf, Pt as RelationView, Q as RelationshipAccessor, Qt as ActionDescriptor, R as FilterQuery, Rt as relatedPaginator, S as TransportResponse, St as ATOMIC_MEDIA_TYPE, T as AtomicResultOf, Tt as MediaTypeParams, U as LinkageRef, Ut as PageKeys, V as IncludePath, Vt as ExactPageParams, W as ListPageParams, Wt as PageLinks, X as RelationPaginator, Xt as TypedPage, Y as RelationPageParams, Yt as ResolvedPaginator, Z as RelationReadQuery, Zt as resolvePaginator, _ as HANDLE_RESERVED, _t as AtomicResults, a as Validator, an as ClientIdPolicy, at as TypeAccessor, b as JsonApiTransport, bt as runAtomic, c as resolveValidator, cn as PaginatorDescriptor, ct as UpdateInput, d as JsonApiContext, dn as PaginatorStrategy, dt as AtomicCreateInput, en as ActionOutput, et as ResourceHandle, f as JsonApiRequest, fn as RelationDescriptor, ft as AtomicDeleteHandle, g as ClientOptions, gt as AtomicResult, h as serializeQuery, hn as StrategyDescriptor, ht as AtomicRef, i as ValidationOption, in as Cardinality, it as SortToken, j as DefaultAttributes, jt as EdgeEnvelope, k as CreateInput, kt as DocumentEnvelope, l as validateDocument, ln as PaginatorKind, lt as WriteOptions, m as execute, mn as ResourceDescriptor, mt as AtomicRecorder, n as SchemaMap, nn as ApiDescriptor, nt as SingleReadQuery, o as WireResource, on as CountableDescriptor, ot as TypeName, p as ReadQuery, pn as RelationMutations, pt as AtomicHandle, q as ReadResult, qt as PageParams, r as ValidationConfig, rn as AtomicDescriptor, rt as SortQuery, s as assertJsonApiDocument, sn as MenuDescriptor, st as TypedReadQuery, t as ResolvedValidator, tn as ActionScope, tt as ResourceObjectView, u as Document, un as PaginatorRole, ut as AtomicCreateHandle, v as HeadersInit, vt as AtomicUpdateHandle, w as ActionsAccessor, wt as LocalIdentifier, x as TransportRequest, xt as ATOMIC_EXT, y as createClient, yt as AtomicUpdateInput, z as HydratedMember, zt as relatedPaginatorKind } from "./validate-DgCUbzgT.mjs";
1
+ import { $ as RelationshipAccessor, $t as ActionDescriptor, A as DefaultActionTypes, At as DocumentEnvelope, B as IdentifierMember, Bt as relatedPaginatorKind, C as fetchTransport, Ct as ATOMIC_MEDIA_TYPE, D as Collection, Dt as ResourceIdentifier, E as Client, Et as MediaTypeParams, F as FieldSelectionMap, Ft as RelationView, G as MemberName, Gt as PageLinks, H as IncludedRelations, Ht as ExactPageParams, I as FieldsMap, It as ResourceEnvelope, J as ReadableRelationName, Jt as PageParams, K as PivotInput, Kt as PageMeta, L as FilterKey, Lt as ResourceLinks, M as DefaultWriteAttributes, Mt as EdgeEnvelope, N as EdgeMembers, Nt as MaterialiseContext, O as CountToken, Ot as mediaType, P as ErrorCodeOf, Pt as NavigateOptions, Q as RelationReadQuery, Qt as resolvePaginator, R as FilterQuery, Rt as materialise, S as TransportResponse, St as ATOMIC_EXT, T as AtomicResultOf, Tt as LocalIdentifier, U as LinkageRef, Ut as Page, V as IncludePath, Vt as CANONICAL_PAGE_KEYS, W as ListPageParams, Wt as PageKeys, X as RelationPageParams, Xt as ResolvedPaginator, Y as RelationName, Yt as PageParamsOf, Z as RelationPaginator, Zt as TypedPage, _ as HANDLE_RESERVED, _n as StrategyDescriptor, _t as AtomicResult, a as Validator, an as Cardinality, at as SortToken, b as JsonApiTransport, bt as AtomicUpdateInput, c as resolveValidator, cn as MenuDescriptor, ct as TypedReadQuery, d as JsonApiContext, dn as PaginatorRole, dt as AtomicCreateHandle, en as ActionInput, et as ResourceAccessors, f as JsonApiRequest, fn as PaginatorStrategy, ft as AtomicCreateInput, g as ClientOptions, gn as ResourceDescriptor, gt as AtomicRef, h as serializeQuery, hn as RelationMutations, ht as AtomicRecorder, i as ValidationOption, in as AtomicDescriptor, it as SortQuery, j as DefaultAttributes, jt as Edge, k as CreateInput, kt as ArrayEnvelope, l as validateDocument, ln as PaginatorDescriptor, lt as UpdateInput, m as execute, mn as RelationDescriptor, mt as AtomicHandle, n as SchemaMap, nn as ActionScope, nt as ResourceObjectView, o as WireResource, on as ClientIdPolicy, ot as TypeAccessor, p as ReadQuery, pn as PivotDescriptor, pt as AtomicDeleteHandle, q as ReadResult, qt as PageOf, r as ValidationConfig, rn as ApiDescriptor, rt as SingleReadQuery, s as assertJsonApiDocument, sn as CountableDescriptor, st as TypeName, t as ResolvedValidator, tn as ActionOutput, tt as ResourceHandle, u as Document, un as PaginatorKind, ut as WriteOptions, v as HeadersInit, vt as AtomicResults, w as ActionsAccessor, wt as JSON_API_MEDIA_TYPE, x as TransportRequest, xt as runAtomic, y as createClient, yt as AtomicUpdateHandle, z as HydratedMember, zt as relatedPaginator } from "./validate-B98ghVgw.mjs";
2
2
 
3
3
  //#region src/errors.d.ts
4
4
  /**
@@ -216,5 +216,5 @@ declare function withRemappedRelationshipPaths(error: JsonApiError, rel: string,
216
216
  */
217
217
  declare function withRemappedAtomicPaths(error: JsonApiError, descriptor: ApiDescriptor, typeForOp: (opIndex: number) => string | undefined): JsonApiError;
218
218
  //#endregion
219
- export { ATOMIC_EXT, ATOMIC_MEDIA_TYPE, ActionDescriptor, ActionInput, ActionOutput, ActionScope, ActionsAccessor, ApiDescriptor, ArrayEnvelope, AtomicCreateHandle, AtomicCreateInput, AtomicDeleteHandle, AtomicDescriptor, AtomicHandle, AtomicRecorder, AtomicRef, AtomicResult, AtomicResultOf, AtomicResults, AtomicUpdateHandle, AtomicUpdateInput, CANONICAL_PAGE_KEYS, Cardinality, Client, ClientIdPolicy, ClientOptions, Collection, CountToken, CountableDescriptor, CreateInput, DefaultActionTypes, DefaultAttributes, DefaultWriteAttributes, Document, DocumentEnvelope, Edge, EdgeEnvelope, EdgeMembers, ErrorCodeGuard, ErrorCodeOf, ExactPageParams, FieldSelectionMap, FieldsMap, FilterKey, FilterQuery, HANDLE_RESERVED, HeadersInit, HydratedMember, IdentifierMember, IncludePath, IncludedRelations, JSON_API_MEDIA_TYPE, JsonApiContext, JsonApiError, JsonApiErrorObject, JsonApiRequest, JsonApiTransport, LinkageRef, ListPageParams, LocalIdentifier, MaterialiseContext, MediaTypeParams, MemberName, MenuDescriptor, NavigateOptions, Page, PageKeys, PageLinks, PageMeta, PageOf, PageParams, PageParamsOf, PaginatorDescriptor, PaginatorKind, PaginatorRole, PaginatorStrategy, PivotInput, ReadQuery, ReadResult, RelationDescriptor, RelationMutations, RelationName, RelationPageParams, RelationPaginator, RelationReadQuery, RelationView, RelationshipAccessor, RelationshipDocument, ResolvedPaginator, ResolvedValidator, ResourceAccessors, ResourceDescriptor, ResourceEnvelope, ResourceHandle, ResourceIdentifier, ResourceLinks, ResourceObjectView, SchemaMap, SingleReadQuery, SortQuery, SortToken, StrategyDescriptor, StructuralGuardError, ToDocumentOptions, TransportRequest, TransportResponse, TypeAccessor, TypeName, TypedPage, TypedReadQuery, UpdateInput, ValidationConfig, ValidationOption, Validator, WireResource, WriteDocument, WriteOptions, assertJsonApiDocument, createClient, execute, fetchTransport, hasErrorCode, materialise, mediaType, relatedPaginator, relatedPaginatorKind, remapPointer, remapRelationshipPointer, resolvePaginator, resolveValidator, runAtomic, serializeQuery, toDocument, toRelationshipDocument, validateDocument, withRemappedAtomicPaths, withRemappedPaths, withRemappedRelationshipPaths };
219
+ export { ATOMIC_EXT, ATOMIC_MEDIA_TYPE, ActionDescriptor, ActionInput, ActionOutput, ActionScope, ActionsAccessor, ApiDescriptor, ArrayEnvelope, AtomicCreateHandle, AtomicCreateInput, AtomicDeleteHandle, AtomicDescriptor, AtomicHandle, AtomicRecorder, AtomicRef, AtomicResult, AtomicResultOf, AtomicResults, AtomicUpdateHandle, AtomicUpdateInput, CANONICAL_PAGE_KEYS, Cardinality, Client, ClientIdPolicy, ClientOptions, Collection, CountToken, CountableDescriptor, CreateInput, DefaultActionTypes, DefaultAttributes, DefaultWriteAttributes, Document, DocumentEnvelope, Edge, EdgeEnvelope, EdgeMembers, ErrorCodeGuard, ErrorCodeOf, ExactPageParams, FieldSelectionMap, FieldsMap, FilterKey, FilterQuery, HANDLE_RESERVED, HeadersInit, HydratedMember, IdentifierMember, IncludePath, IncludedRelations, JSON_API_MEDIA_TYPE, JsonApiContext, JsonApiError, JsonApiErrorObject, JsonApiRequest, JsonApiTransport, LinkageRef, ListPageParams, LocalIdentifier, MaterialiseContext, MediaTypeParams, MemberName, MenuDescriptor, NavigateOptions, Page, PageKeys, PageLinks, PageMeta, PageOf, PageParams, PageParamsOf, PaginatorDescriptor, PaginatorKind, PaginatorRole, PaginatorStrategy, PivotDescriptor, PivotInput, ReadQuery, ReadResult, ReadableRelationName, RelationDescriptor, RelationMutations, RelationName, RelationPageParams, RelationPaginator, RelationReadQuery, RelationView, RelationshipAccessor, RelationshipDocument, ResolvedPaginator, ResolvedValidator, ResourceAccessors, ResourceDescriptor, ResourceEnvelope, ResourceHandle, ResourceIdentifier, ResourceLinks, ResourceObjectView, SchemaMap, SingleReadQuery, SortQuery, SortToken, StrategyDescriptor, StructuralGuardError, ToDocumentOptions, TransportRequest, TransportResponse, TypeAccessor, TypeName, TypedPage, TypedReadQuery, UpdateInput, ValidationConfig, ValidationOption, Validator, WireResource, WriteDocument, WriteOptions, assertJsonApiDocument, createClient, execute, fetchTransport, hasErrorCode, materialise, mediaType, relatedPaginator, relatedPaginatorKind, remapPointer, remapRelationshipPointer, resolvePaginator, resolveValidator, runAtomic, serializeQuery, toDocument, toRelationshipDocument, validateDocument, withRemappedAtomicPaths, withRemappedPaths, withRemappedRelationshipPaths };
220
220
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -539,12 +539,31 @@ const ATOMIC_MEDIA_TYPE = mediaType({ ext: [ATOMIC_EXT] });
539
539
  //#region src/request.ts
540
540
  const isAbsolute = (path) => /^https?:\/\//i.test(path);
541
541
  const RELATIVE_ORIGIN = "http://relative.invalid";
542
- /** Resolve a server link against `baseUrl` per RFC 3986. An absolute link is kept verbatim. */
542
+ /**
543
+ * Resolve a server link against `baseUrl` per RFC 3986 and check where it lands. A link on the
544
+ * base's origin is followed; a relative base (e.g. `/api`) takes the page's origin in a browser and
545
+ * otherwise only accepts a link that keeps the host. Anything that isn't plain http(s) is refused.
546
+ */
543
547
  function resolveLink(link, baseUrl) {
544
- if (isAbsolute(link)) return link;
545
- if (isAbsolute(baseUrl)) return new URL(link, baseUrl).href;
546
- const resolved = new URL(link, new URL(baseUrl, RELATIVE_ORIGIN));
547
- return resolved.origin === RELATIVE_ORIGIN ? resolved.pathname + resolved.search + resolved.hash : resolved.href;
548
+ let normalised = link.replace(/[\t\n\r]/g, "");
549
+ let start = 0;
550
+ while (start < normalised.length && normalised.charCodeAt(start) <= 32) start++;
551
+ normalised = normalised.slice(start);
552
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(normalised)?.[1]?.toLowerCase();
553
+ if (scheme !== void 0 && scheme !== "http" && scheme !== "https") throw new Error(`Refusing to follow link "${link}": only http(s) links are followed`);
554
+ const relativeBase = !isAbsolute(baseUrl);
555
+ const page = globalThis.location?.href;
556
+ const base = new URL(baseUrl, relativeBase ? typeof page === "string" ? page : RELATIVE_ORIGIN : void 0);
557
+ const resolved = new URL(link, base);
558
+ const sameOrigin = resolved.origin === base.origin && (scheme === void 0 || /^https?:\/\//i.test(normalised));
559
+ if (!sameOrigin) return {
560
+ url: resolved.href,
561
+ sameOrigin
562
+ };
563
+ return {
564
+ url: relativeBase ? resolved.pathname + resolved.search + resolved.hash : resolved.href,
565
+ sameOrigin
566
+ };
548
567
  }
549
568
  const append = (parts, key, value) => {
550
569
  if (value === void 0 || value === null || value === "") return;
@@ -582,14 +601,21 @@ function serializeQuery(query) {
582
601
  return parts.join("&");
583
602
  }
584
603
  /**
585
- * Execute a request: build the URL (a server link resolved against `baseUrl`, an absolute path
586
- * verbatim, else `baseUrl + path`), apply content negotiation and any async per-request headers,
604
+ * Execute a request: build the URL (a server link resolved against `baseUrl` and held to its
605
+ * origin, an absolute path verbatim, else `baseUrl + path`), apply content negotiation and any async per-request headers,
587
606
  * then drive the transport. A 2xx with a body parses to a {@link Document}; an empty /
588
607
  * 204 body resolves to `undefined`. A non-2xx throws a {@link JsonApiError} carrying the
589
608
  * parsed error objects (tolerating a non-JSON error body with an empty list).
590
609
  */
591
610
  async function execute(ctx, req) {
592
- let url = req.link ? resolveLink(req.path, ctx.baseUrl) : isAbsolute(req.path) ? req.path : ctx.baseUrl + req.path;
611
+ let url;
612
+ let sendHeaders = true;
613
+ if (req.link) {
614
+ const link = resolveLink(req.path, ctx.baseUrl);
615
+ if (!link.sameOrigin && ctx.allowCrossOriginLinks !== true) throw new Error(`Refusing to follow link "${req.path}": it leaves the base URL's origin (set allowCrossOriginLinks to follow it without the configured headers)`);
616
+ url = link.url;
617
+ sendHeaders = link.sameOrigin;
618
+ } else url = isAbsolute(req.path) ? req.path : ctx.baseUrl + req.path;
593
619
  if (req.query) {
594
620
  const qs = serializeQuery(req.query);
595
621
  if (qs) url += (url.includes("?") ? "&" : "?") + qs;
@@ -609,7 +635,7 @@ async function execute(ctx, req) {
609
635
  headers["Content-Type"] = req.contentType ?? negotiated;
610
636
  transportReq.body = req.raw && typeof req.body === "string" ? req.body : JSON.stringify(req.body);
611
637
  }
612
- if (ctx.headers) mergeHeaders(headers, await ctx.headers());
638
+ if (ctx.headers && sendHeaders) mergeHeaders(headers, await ctx.headers());
613
639
  const res = await ctx.transport(transportReq);
614
640
  if (res.status >= 200 && res.status < 300) return parseDocument(res.body);
615
641
  throw new JsonApiError(res.status, parseErrors(res.body));
@@ -1019,6 +1045,18 @@ const fetchTransport = async (req) => {
1019
1045
  /** Substitute `{key}` placeholders in a path template (`/albums/{id}/{rel}` -> `/albums/1/tracks`). */
1020
1046
  const fill = (template, vars) => template.replace(/\{(\w+)\}/g, (_, key) => vars[key] ?? `{${key}}`);
1021
1047
  /**
1048
+ * Fill a type's path template, putting a relation's URI segment (its `uriField`, else its name)
1049
+ * in place of `{rel}`. `vars.rel` stays the relation's name everywhere else.
1050
+ */
1051
+ function fillPath(ctx, type, template, vars) {
1052
+ const rel = vars["rel"];
1053
+ const uriField = rel === void 0 ? void 0 : ctx.descriptor[type]?.relations[rel]?.uriField;
1054
+ return fill(template, uriField === void 0 ? vars : {
1055
+ ...vars,
1056
+ rel: uriField
1057
+ });
1058
+ }
1059
+ /**
1022
1060
  * Run an operation: resolve the path template from the type's descriptor, fill its
1023
1061
  * placeholders, execute, and materialise the resulting document. A no-body / 204 response
1024
1062
  * materialises to `undefined`; an operation the type does not declare (no path template) throws
@@ -1031,7 +1069,7 @@ async function run(ctx, type, operation, vars, query, linkage = false, relationF
1031
1069
  if (template === void 0) throw new Error(`No "${operation}" path declared for type "${type}"`);
1032
1070
  const req = {
1033
1071
  method: "GET",
1034
- path: fill(template, vars)
1072
+ path: fillPath(ctx, type, template, vars)
1035
1073
  };
1036
1074
  if (query !== void 0) {
1037
1075
  const { signal, ...params } = query;
@@ -1081,7 +1119,7 @@ async function runWrite(ctx, spec) {
1081
1119
  if (template === void 0) throw new Error(`No "${spec.operation}" path declared for type "${spec.type}"`);
1082
1120
  const req = {
1083
1121
  method: spec.method,
1084
- path: fill(template, spec.vars),
1122
+ path: fillPath(ctx, spec.type, template, spec.vars),
1085
1123
  body: spec.body
1086
1124
  };
1087
1125
  if (spec.query !== void 0) req.query = spec.query;
@@ -1180,8 +1218,9 @@ function relationshipAccessor(ctx, type, id, rel) {
1180
1218
  return mutations === void 0 ? true : mutations[verb] === true;
1181
1219
  };
1182
1220
  const accessor = {};
1183
- if (relation?.relationship !== false) accessor["get"] = (query) => run(ctx, type, "fetchRelationship", vars, query, true, rel);
1184
- if (relation?.related !== false) accessor["related"] = (query) => run(ctx, type, "fetchRelated", vars, query, false, rel);
1221
+ const readable = relation?.writeOnly !== true;
1222
+ if (readable && relation?.relationship !== false) accessor["get"] = (query) => run(ctx, type, "fetchRelationship", vars, query, true, rel);
1223
+ if (readable && relation?.related !== false) accessor["related"] = (query) => run(ctx, type, "fetchRelated", vars, query, false, rel);
1185
1224
  if (cardinality === "many") {
1186
1225
  if (advertises("add")) accessor["add"] = (refs) => mutate("POST", refs);
1187
1226
  if (advertises("remove")) accessor["remove"] = (refs) => mutate("DELETE", refs);
@@ -1266,7 +1305,8 @@ function createClient(descriptor, options) {
1266
1305
  const request = {
1267
1306
  baseUrl: options.baseUrl,
1268
1307
  transport: options.transport ?? fetchTransport,
1269
- ...options.headers !== void 0 ? { headers: options.headers } : {}
1308
+ ...options.headers !== void 0 ? { headers: options.headers } : {},
1309
+ ...options.allowCrossOriginLinks !== void 0 ? { allowCrossOriginLinks: options.allowCrossOriginLinks } : {}
1270
1310
  };
1271
1311
  const validator = resolveValidator(options.validate);
1272
1312
  const ctx = {