@rivinia/http 0.5.3 → 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0
4
+
5
+ - First stable release. URL joining collapses repeated boundary slashes; request URLs otherwise remain consumer-controlled.
6
+
7
+ ## Unreleased
8
+
9
+ - Fix `joinUrl` so repeated leading and trailing slashes are collapsed to one boundary slash while query strings are preserved.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rivinia contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -3,12 +3,16 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@rivinia/http.svg)](https://www.npmjs.com/package/@rivinia/http)
4
4
  [![license](https://img.shields.io/npm/l/@rivinia/http.svg)](https://www.npmjs.com/package/@rivinia/http)
5
5
 
6
+ <p align="center">
7
+ <img src="https://raw.githubusercontent.com/abdelfattahqandil21-oss/Revinia/http/v1.0.0/assets/rivinia-abstract-mark-light.svg" alt="Rivinia" width="340" />
8
+ </p>
9
+
6
10
  `@rivinia/http` is a thin, typed Angular HTTP client for plain `Observable`
7
11
  requests and reactive `httpResource` requests. It leaves URLs, backend
8
12
  contracts, and interceptors under app control. Response bodies pass through
9
13
  unchanged unless a request explicitly asks to unwrap one.
10
14
 
11
- - Package version: **0.5.2**
15
+ - Package version: **1.0.0**
12
16
  - Requires Angular `^22.0.0` and RxJS `^6.5.3 || ^7.4.0`; `tslib` is installed
13
17
  as a dependency.
14
18
 
@@ -402,3 +406,7 @@ call sites. Without the per-request options, the response remains raw.
402
406
  ## License
403
407
 
404
408
  MIT.
409
+
410
+ ## Further reading
411
+
412
+ [Package design and usage notes](https://github.com/abdelfattahqandil21-oss/Revinia/blob/http/v1.0.0/docs/decisions/D026-consumer-driven-http-0-3-0-reactive-function-endpoints.md).
@@ -4,8 +4,8 @@ import { HttpClient, HttpContext, HttpParams, httpResource, provideHttpClient }
4
4
  import { map } from "rxjs";
5
5
  function joinUrl(baseUrl, endpoint) {
6
6
  if (/^https?:\/\//i.test(endpoint)) return endpoint;
7
- const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
8
- const path = endpoint.startsWith("/") ? endpoint.slice(1) : endpoint;
7
+ const base = baseUrl.replace(/\/+$/, "");
8
+ const path = endpoint.replace(/^\/+/, "");
9
9
  return path ? `${base}/${path}` : base;
10
10
  }
11
11
  var RiviniaHttpUnwrapError = class extends Error {
@@ -165,7 +165,7 @@ var RiviniaHttp = class RiviniaHttp {
165
165
  }
166
166
  static ɵfac = i0.ɵɵngDeclareFactory({
167
167
  minVersion: "12.0.0",
168
- version: "22.2.2",
168
+ version: "22.2.3",
169
169
  ngImport: i0,
170
170
  type: RiviniaHttp,
171
171
  deps: [],
@@ -173,14 +173,14 @@ var RiviniaHttp = class RiviniaHttp {
173
173
  });
174
174
  static ɵprov = i0.ɵɵngDeclareService({
175
175
  minVersion: "22.0.0",
176
- version: "22.2.2",
176
+ version: "22.2.3",
177
177
  ngImport: i0,
178
178
  type: RiviniaHttp
179
179
  });
180
180
  };
181
181
  i0.ɵɵngDeclareClassMetadata({
182
182
  minVersion: "12.0.0",
183
- version: "22.2.2",
183
+ version: "22.2.3",
184
184
  ngImport: i0,
185
185
  type: RiviniaHttp,
186
186
  decorators: [{ type: Service }]
@@ -1 +1 @@
1
- {"version":3,"file":"rivinia-http.mjs","names":[],"sources":["../../../projects/http/src/lib/http-utils.ts","../../../projects/http/src/lib/http-envelope.ts","../../../projects/http/src/lib/http-config.ts","../../../projects/http/src/lib/rivinia-http.ts","../../../projects/http/src/lib/provide-http.ts"],"sourcesContent":["/**\n * Joins a base URL and an endpoint without producing double slashes.\n * Absolute `http(s)` endpoints pass through untouched.\n */\nexport function joinUrl(baseUrl: string, endpoint: string): string {\n if (/^https?:\\/\\//i.test(endpoint)) {\n return endpoint;\n }\n const base = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;\n const path = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint;\n return path ? `${base}/${path}` : base;\n}\n\n/**\n * Typed extraction failure. Thrown when explicit unwrapping\n * (`{ unwrap: true, unwrapKey }`) cannot be performed: empty key, non-object\n * body, or missing property. Never thrown when unwrapping is disabled.\n * In `getResource` this surfaces as `resource.error()`.\n */\nexport class RiviniaHttpUnwrapError extends Error {\n /** The requested envelope key. */\n readonly key: string;\n /** The endpoint being read, when known (never the body). */\n readonly endpoint: string | undefined;\n /**\n * The raw response body. Attached for diagnosis (a `parse` hook or logger\n * can inspect it) — it may carry PII, so never log it blindly and never\n * interpolate it into messages.\n */\n readonly body: unknown;\n\n constructor(key: string, body: unknown, endpoint?: string, reason?: string) {\n super(\n `[rivinia/http] Cannot unwrap key \"${key}\"` +\n (endpoint ? ` from ${endpoint}` : '') +\n (reason ? `: ${reason}` : ': property missing'),\n );\n this.name = 'RiviniaHttpUnwrapError';\n this.key = key;\n this.endpoint = endpoint;\n this.body = body;\n }\n}\n\n/**\n * Strict single-level extraction shared by every unwrapping path (plain\n * methods + `getResource`). Rules:\n * - the key must be a non-empty string (an empty key is never intentional);\n * - the body must be a non-null, non-array object;\n * - presence is an OWN-property check (`hasOwnProperty`) — inherited\n * properties (`'toString' in {}`) never count;\n * - a present-but-falsy value (`null`, `undefined`, `false`, `0`, `''`) is\n * delivered unchanged — truthiness never decides success;\n * - anything else throws `RiviniaHttpUnwrapError`. The original body is\n * never silently returned after an explicit extraction fails.\n *\n * Limit, stated openly: presence proves nothing about the value matching an\n * arbitrary generic `T`. Pair with `envelope` validation or a `parse` hook\n * (Zod/Valibot) when the backend is untyped.\n */\nexport function extractKey(body: unknown, key: string, endpoint?: string): unknown {\n if (typeof key !== 'string' || key.length === 0) {\n throw new RiviniaHttpUnwrapError(key, body, endpoint, 'invalid key');\n }\n if (typeof body !== 'object' || body === null || Array.isArray(body)) {\n throw new RiviniaHttpUnwrapError(key, body, endpoint, 'response is not an object');\n }\n const record = body as Record<string, unknown>;\n if (!Object.prototype.hasOwnProperty.call(record, key)) {\n throw new RiviniaHttpUnwrapError(key, body, endpoint);\n }\n return record[key];\n}\n\n/**\n * Extracts `body[key]` when the backend wraps payloads in an envelope\n * (e.g. `{ data, error, isSuccess }`). Strict: a missing key, a non-object\n * body, or an empty key throws `RiviniaHttpUnwrapError` — the original body\n * is never silently passed through after an explicit extraction request.\n * Unwraps EXACTLY ONE level: a payload that itself contains the key\n * (`{ data: { data: […], page: 1 } }`) is returned whole.\n */\nexport function unwrapAs<T>(body: unknown, key: string): T {\n return extractKey(body, key) as T;\n}\n","/**\n * Typed envelope failure. Thrown when a backend signals failure inside a\n * successful HTTP response (e.g. `{ data: null, isSuccess: false }` with\n * HTTP 200) and the envelope policy opts into throwing. In `getResource`\n * this surfaces as `resource.error()`.\n */\nexport class RiviniaHttpEnvelopeError extends Error {\n /** The raw response body. */\n readonly body: unknown;\n /** The machine-readable failure code (`body[errorKey]`), if any. */\n readonly code: unknown;\n\n constructor(body: unknown, code: unknown) {\n super(\n typeof code === 'string' && code.length > 0\n ? `[rivinia/http] Request failed: ${code}`\n : '[rivinia/http] Request failed (backend envelope)',\n );\n this.name = 'RiviniaHttpEnvelopeError';\n this.body = body;\n this.code = code;\n }\n}\n\n/**\n * Opt-in backend-envelope validation. When present (globally or per call),\n * a response object whose `successKey` is strictly `false` throws\n * `RiviniaHttpEnvelopeError` instead of flowing into `next()` as success.\n * Runs on the raw body before unwrapping, independent of `unwrap`.\n */\nexport interface RiviniaEnvelopePolicy {\n /** Body key holding the success flag. @default 'isSuccess' */\n readonly successKey?: string;\n /** Body key holding the failure code. @default 'error' */\n readonly errorKey?: string;\n /** Throw on failure. Explicit `false` disables (policy becomes passive). @default true */\n readonly throwOnFailure?: boolean;\n}\n\nconst DEFAULT_SUCCESS_KEY = 'isSuccess';\nconst DEFAULT_ERROR_KEY = 'error';\n\n/** Throws when the envelope signals failure and the policy opts in. */\nexport function assertEnvelope(body: unknown, policy: RiviniaEnvelopePolicy): void {\n if (policy.throwOnFailure === false) return;\n if (typeof body !== 'object' || body === null || Array.isArray(body)) return;\n const successKey = policy.successKey ?? DEFAULT_SUCCESS_KEY;\n const record = body as Record<string, unknown>;\n if (!(successKey in record) || record[successKey] !== false) return;\n const errorKey = policy.errorKey ?? DEFAULT_ERROR_KEY;\n throw new RiviniaHttpEnvelopeError(body, record[errorKey]);\n}\n","import type { HttpFeature, HttpFeatureKind } from '@angular/common/http';\nimport { InjectionToken } from '@angular/core';\nimport type { RiviniaEnvelopePolicy } from './http-envelope';\n\n/**\n * Global configuration for `@rivinia/http`.\n *\n * Unwrapping is strictly per request (`{ unwrap: true, unwrapKey }` on the\n * call) — there is intentionally no global unwrap configuration. A global\n * key would silently change what every call returns; an explicit key keeps\n * each call honest. URLs are ALWAYS used as given — the library never joins,\n * prefixes, or rewrites them. Build them at the call site from wherever you\n * want (`environment.apiUrl`, a constant, a signal …) with the exported\n * `joinUrl`.\n */\nexport interface RiviniaHttpConfig {\n /**\n * Optional `HttpClient` features (`withInterceptors(...)`, `withXhr()`, …),\n * applied via a single internal `provideHttpClient(...httpFeatures)` call.\n *\n * Use this INSTEAD of calling `provideHttpClient()` yourself — calling both\n * registers the built-in XSRF interceptor twice (verified against Angular\n * 22.2.2 source: `xsrfInterceptorFn` is a multi-provider with no cross-call\n * dedup). Omit it entirely on Angular ≥21 when you need no features:\n * `HttpClient` (fetch backend) is injectable by default.\n */\n httpFeatures?: HttpFeature<HttpFeatureKind>[];\n /**\n * Opt-in backend-envelope validation applied to every call unless the call\n * provides its own `envelope` (which replaces this wholesale). See\n * `RiviniaEnvelopePolicy`. Off by default — plain bodies flow untouched.\n */\n envelope?: RiviniaEnvelopePolicy;\n}\n\n/**\n * Internal configuration token. Consumers must use `provideRiviniaHttp()`.\n * @internal\n */\nexport const RIVINIA_HTTP_CONFIG = new InjectionToken<RiviniaHttpConfig>('RIVINIA_HTTP_CONFIG');\n","import {\n HttpClient,\n HttpContext,\n HttpParams,\n httpResource,\n type HttpHeaders,\n type HttpResourceRef,\n type HttpResponse,\n} from '@angular/common/http';\nimport { Service, computed, inject, isSignal, type Signal } from '@angular/core';\nimport { map, type Observable } from 'rxjs';\nimport { RIVINIA_HTTP_CONFIG, type RiviniaHttpConfig } from './http-config';\nimport type {\n QueryParamsInput,\n ReactiveInput,\n RiviniaDeleteRequestOptions,\n RiviniaRawOptions,\n RiviniaRequestBase,\n RiviniaRequestOptions,\n RiviniaUnwrapOff,\n RiviniaUnwrapOn,\n} from './http-options';\nimport { extractKey } from './http-utils';\nimport { assertEnvelope, type RiviniaEnvelopePolicy } from './http-envelope';\n\n/**\n * Reactive per-request fields for `getResource` (re-evaluated on every\n * request; everything else is read once per call site).\n */\nexport interface RiviniaResourceBase<T> {\n /**\n * Query params re-evaluated on every request: value, signal, or reactive\n * function. Anything else in these options is read once per call site —\n * only the URL, params, headers and context are reactive.\n */\n params?: ReactiveInput<QueryParamsInput>;\n headers?: ReactiveInput<HttpHeaders | Record<string, string | string[]>>;\n context?: ReactiveInput<HttpContext>;\n withCredentials?: boolean;\n /** Value while idle/loading. Defaults to `undefined`. */\n defaultValue?: T;\n /**\n * Runs AFTER envelope unwrapping: `raw -> unwrap -> parse`.\n * Use for runtime validation (e.g. Zod) or shaping.\n */\n parse?: (value: unknown) => T;\n /**\n * Forwarded to the `httpResource` request object only (enables the\n * resource `progress` signal for downloads). Has no effect on the\n * plain Observable methods — use `HttpClient` events there directly.\n */\n reportProgress?: boolean;\n /**\n * Opt-in backend-envelope validation for this resource (replaces any\n * global `envelope` policy wholesale). See `RiviniaEnvelopePolicy`.\n */\n envelope?: RiviniaEnvelopePolicy;\n}\n\n/**\n * `getResource` options: reactive base + the same per-call unwrap contract\n * as the plain methods (composed explicitly — `Omit` over a union would\n * destroy the discrimination).\n */\nexport type RiviniaResourceOptions<T> = RiviniaResourceBase<T> &\n (RiviniaUnwrapOn | RiviniaUnwrapOff);\n\n/**\n * Headless typed HTTP client.\n *\n * Thin over `HttpClient` (so app interceptors, `HttpContext` escape hatches\n * and `provideHttpClient()` testing all keep working). Owns no backend:\n * URLs pass through untouched; the library adds opt-in envelope handling —\n * per-request unwrapping (`{ unwrap: true, unwrapKey }`, mandatory key, no\n * default) and failure validation (`envelope`).\n */\n@Service()\nexport class RiviniaHttp {\n private readonly http = inject(HttpClient);\n private readonly config = inject<RiviniaHttpConfig>(RIVINIA_HTTP_CONFIG, {\n optional: true,\n });\n\n get<T>(endpoint: string, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .get<unknown>(this.url(endpoint), this.httpOptions(options))\n .pipe(map((body) => this.applyUnwrap<T>(endpoint, body, options)));\n }\n\n post<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .post<unknown>(this.url(endpoint), body, this.httpOptions(options))\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n put<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .put<unknown>(this.url(endpoint), body, this.httpOptions(options))\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n patch<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .patch<unknown>(this.url(endpoint), body, this.httpOptions(options))\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n delete<T>(endpoint: string, options?: RiviniaDeleteRequestOptions): Observable<T> {\n return this.http\n .delete<unknown>(this.url(endpoint), {\n ...this.httpOptions(options),\n body: options?.body,\n })\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n /** Binary download. Never unwrapped — envelopes don't apply to blobs. */\n getBlob(endpoint: string, options?: RiviniaRawOptions): Observable<Blob> {\n return this.http.get(this.url(endpoint), {\n ...this.httpOptions(options),\n responseType: 'blob',\n });\n }\n\n /** Plain-text download. Never unwrapped. */\n getText(endpoint: string, options?: RiviniaRawOptions): Observable<string> {\n return this.http.get(this.url(endpoint), {\n ...this.httpOptions(options),\n responseType: 'text',\n });\n }\n\n /**\n * Full response access (status, headers — e.g. pagination counts or a 201\n * `Location`). Raw body (no unwrapping), but the configured/per-call\n * `envelope` validation still applies to the body — a globally declared\n * failure policy must hold on every JSON path, not just the unwrapped ones.\n */\n getResponse<T>(endpoint: string, options?: RiviniaRawOptions): Observable<HttpResponse<T>> {\n const envelope = this.resolveEnvelope(options);\n return this.http\n .get<T>(this.url(endpoint), {\n ...this.httpOptions(options),\n observe: 'response',\n })\n .pipe(\n map((res) => {\n if (envelope) assertEnvelope(res.body, envelope);\n return res;\n }),\n );\n }\n\n /**\n * Full response access for creates (status, `Location` header). Same\n * envelope-validation rule as `getResponse`; body otherwise raw.\n */\n postResponse<T>(\n endpoint: string,\n body?: unknown,\n options?: RiviniaRawOptions,\n ): Observable<HttpResponse<T>> {\n const envelope = this.resolveEnvelope(options);\n return this.http\n .post<T>(this.url(endpoint), body, {\n ...this.httpOptions(options),\n observe: 'response',\n })\n .pipe(\n map((res) => {\n if (envelope) assertEnvelope(res.body, envelope);\n return res;\n }),\n );\n }\n\n /**\n * Reactive GET as an `httpResource` (stable since Angular v22).\n * The endpoint can be a static string, a signal, or a reactive function\n * (same as `httpResource` itself) — it re-fetches on change, and\n * `undefined` disables the request. Unwrap applies through `parse`,\n * composed before a user `parse` if given.\n */\n getResource<T>(\n endpoint: string | Signal<string | undefined> | (() => string | undefined),\n options?: RiviniaResourceOptions<T>,\n ): HttpResourceRef<T | undefined> {\n const { defaultValue, parse, reportProgress, ...requestOptions } = options ?? {};\n const source: string | Signal<string | undefined> =\n typeof endpoint === 'function' && !isSignal(endpoint)\n ? computed(endpoint)\n : (endpoint as string | Signal<string | undefined>);\n const request = () => {\n const ep = typeof source === 'string' ? source : source();\n if (ep === undefined) return undefined;\n return {\n url: this.url(ep),\n params: this.toParams(resolveInput(requestOptions.params)),\n headers: resolveInput(requestOptions.headers),\n context: resolveInput(requestOptions.context),\n withCredentials: requestOptions.withCredentials,\n ...(reportProgress !== undefined ? { reportProgress } : {}),\n };\n };\n const envelope = this.resolveEnvelope(requestOptions);\n const base = { defaultValue: defaultValue as T | undefined };\n // Pipeline order: envelope validation (raw) -> unwrap -> user parse.\n // A throwing envelope or a failed extraction surfaces as resource.error().\n const applyParse = (raw: unknown): T => {\n if (envelope) assertEnvelope(raw, envelope);\n if (requestOptions.unwrap !== true) return parse ? parse(raw as T) : (raw as T);\n const unwrapped = extractKey(raw, requestOptions.unwrapKey) as T;\n return parse ? parse(unwrapped) : unwrapped;\n };\n return defaultValue === undefined\n ? httpResource<T>(request, { ...base, parse: applyParse })\n : httpResource<T>(request, {\n ...base,\n defaultValue: defaultValue as T,\n parse: applyParse,\n });\n }\n\n private url(endpoint: string): string {\n // Deliberate passthrough: the caller owns the URL (environment, constant,\n // signal …). Use the exported `joinUrl` at the call site when composing.\n return endpoint;\n }\n\n private resolveEnvelope(options?: {\n envelope?: RiviniaEnvelopePolicy;\n }): RiviniaEnvelopePolicy | undefined {\n return options?.envelope ?? this.config?.envelope;\n }\n\n private applyUnwrap<T>(endpoint: string, body: unknown, options?: RiviniaRequestOptions): T {\n const envelope = this.resolveEnvelope(options);\n if (envelope) assertEnvelope(body, envelope);\n if (options?.unwrap !== true) return body as T;\n return extractKey(body, options.unwrapKey, endpoint) as T;\n }\n\n private httpOptions(options?: RiviniaRequestBase) {\n return {\n params: this.toParams(options?.params),\n headers: options?.headers,\n context: options?.context ?? new HttpContext(),\n withCredentials: options?.withCredentials,\n };\n }\n\n private toParams(input?: QueryParamsInput): HttpParams | undefined {\n if (!input) return undefined;\n if (input instanceof HttpParams) return input;\n let params = new HttpParams();\n for (const [key, value] of Object.entries(input)) {\n if (value !== undefined && value !== null) {\n params = params.set(key, String(value));\n }\n }\n return params;\n }\n}\n\n/**\n * Evaluates a reactive input inside the request function: signals are read\n * (tracked), plain functions are called, values pass through.\n */\nfunction resolveInput<T>(value: ReactiveInput<T> | undefined): T | undefined {\n if (value === undefined) return undefined;\n if (isSignal(value)) return (value as Signal<T>)();\n if (typeof value === 'function') return (value as () => T)();\n return value as T;\n}\n","import { EnvironmentProviders, inject, makeEnvironmentProviders } from '@angular/core';\nimport { provideHttpClient } from '@angular/common/http';\nimport { RIVINIA_HTTP_CONFIG, type RiviniaHttpConfig } from './http-config';\nimport { RiviniaHttp } from './rivinia-http';\n\n/**\n * Configures `@rivinia/http`.\n *\n * URLs are always used as given — build them at the call site from wherever\n * you want (see the exported `joinUrl` + your `environment.apiUrl`).\n *\n * Three backend styles (pick ONE):\n * - `provideRiviniaHttp()` alone — Angular ≥21 already injects `HttpClient` (fetch).\n * - `provideRiviniaHttp({ httpFeatures: [withInterceptors([...])] })` — single\n * call that also configures the backend. Do NOT additionally call\n * `provideHttpClient()` (double registration duplicates the XSRF interceptor).\n * - `provideHttpClient(...)` + `provideRiviniaHttp()` separately — equivalent,\n * keeps backend config visibly in app code (this is what `ng add` generates).\n *\n * @example\n * ```ts\n * provideRiviniaHttp()\n * ```\n */\nexport function provideRiviniaHttp(config: RiviniaHttpConfig = {}): EnvironmentProviders {\n const { httpFeatures, ...rest } = config;\n return makeEnvironmentProviders([\n ...(httpFeatures ? [provideHttpClient(...httpFeatures)] : []),\n { provide: RIVINIA_HTTP_CONFIG, useValue: rest },\n ]);\n}\n\n/** Shorthand for `inject(RiviniaHttp)`. */\nexport function injectRiviniaHttp(): RiviniaHttp {\n return inject(RiviniaHttp);\n}\n"],"mappings":";;;;AAIA,SAAgB,QAAQ,SAAiB,UAAgB;CACvD,IAAI,gBAAgB,KAAK,QAAQ,GAC/B,OAAO;CAET,MAAM,OAAO,QAAQ,SAAS,GAAG,IAAI,QAAQ,MAAM,GAAG,EAAE,IAAI;CAC5D,MAAM,OAAO,SAAS,WAAW,GAAG,IAAI,SAAS,MAAM,CAAC,IAAI;CAC5D,OAAO,OAAO,GAAG,KAAI,GAAI,SAAS;AACpC;AAQA,IAAa,yBAAb,cAA4C,MAAK;CAEtC;CAEA;CAMA;CAET,YAAY,KAAa,MAAe,UAAmB,QAAe;EACxE,MACE,qCAAqC,IAAG,MACrC,WAAW,SAAS,aAAa,OACjC,SAAS,KAAK,WAAW,qBAAqB;EAEnD,KAAK,OAAO;EACZ,KAAK,MAAM;EACX,KAAK,WAAW;EAChB,KAAK,OAAO;CACd;AACD;AAkBD,SAAgB,WAAW,MAAe,KAAa,UAAiB;CACtE,IAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,GAC5C,MAAM,IAAI,uBAAuB,KAAK,MAAM,UAAU,aAAa;CAErE,IAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,MAAM,QAAQ,IAAI,GACjE,MAAM,IAAI,uBAAuB,KAAK,MAAM,UAAU,2BAA2B;CAEnF,MAAM,SAAS;CACf,IAAI,CAAC,OAAO,UAAU,eAAe,KAAK,QAAQ,GAAG,GACnD,MAAM,IAAI,uBAAuB,KAAK,MAAM,QAAQ;CAEtD,OAAO,OAAO;AAChB;AAUA,SAAgB,SAAY,MAAe,KAAW;CACpD,OAAO,WAAW,MAAM,GAAG;AAC7B;AC9EA,IAAa,2BAAb,cAA8C,MAAK;CAExC;CAEA;CAET,YAAY,MAAe,MAAa;EACtC,MACE,OAAO,SAAS,YAAY,KAAK,SAAS,IACtC,kCAAkC,SAClC,kDAAkD;EAExD,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK,OAAO;CACd;AACD;AAiBD,MAAM,sBAAsB;AAC5B,MAAM,oBAAoB;AAG1B,SAAgB,eAAe,MAAe,QAA6B;CACzE,IAAI,OAAO,mBAAmB,OAAO;CACrC,IAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,MAAM,QAAQ,IAAI,GAAG;CACtE,MAAM,aAAa,OAAO,cAAc;CACxC,MAAM,SAAS;CACf,IAAI,EAAE,cAAc,WAAW,OAAO,gBAAgB,OAAO;CAE7D,MAAM,IAAI,yBAAyB,MAAM,OADxB,OAAO,YAAY,kBACqB;AAC3D;ACZA,MAAa,sBAAsB,IAAI,eAAkC,qBAAqB;ACsC9F,IAAa,cAAb,MAAa,YAAW;CACL,OAAO,OAAO,UAAU;CACxB,SAAS,OAA0B,qBAAqB,EACvE,UAAU,KACX,CAAA;CAED,IAAO,UAAkB,SAA+B;EACtD,OAAO,KAAK,KACT,IAAa,KAAK,IAAI,QAAQ,GAAG,KAAK,YAAY,OAAO,CAAC,CAAC,CAC3D,KAAK,KAAK,SAAS,KAAK,YAAe,UAAU,MAAM,OAAO,CAAC,CAAC;CACrE;CAEA,KAAQ,UAAkB,MAAgB,SAA+B;EACvE,OAAO,KAAK,KACT,KAAc,KAAK,IAAI,QAAQ,GAAG,MAAM,KAAK,YAAY,OAAO,CAAC,CAAC,CAClE,KAAK,KAAK,QAAQ,KAAK,YAAe,UAAU,KAAK,OAAO,CAAC,CAAC;CACnE;CAEA,IAAO,UAAkB,MAAgB,SAA+B;EACtE,OAAO,KAAK,KACT,IAAa,KAAK,IAAI,QAAQ,GAAG,MAAM,KAAK,YAAY,OAAO,CAAC,CAAC,CACjE,KAAK,KAAK,QAAQ,KAAK,YAAe,UAAU,KAAK,OAAO,CAAC,CAAC;CACnE;CAEA,MAAS,UAAkB,MAAgB,SAA+B;EACxE,OAAO,KAAK,KACT,MAAe,KAAK,IAAI,QAAQ,GAAG,MAAM,KAAK,YAAY,OAAO,CAAC,CAAC,CACnE,KAAK,KAAK,QAAQ,KAAK,YAAe,UAAU,KAAK,OAAO,CAAC,CAAC;CACnE;CAEA,OAAU,UAAkB,SAAqC;EAC/D,OAAO,KAAK,KACT,OAAgB,KAAK,IAAI,QAAQ,GAAG;GACnC,GAAG,KAAK,YAAY,OAAO;GAC3B,MAAM,SAAS;EAChB,CAAA,CAAC,CACD,KAAK,KAAK,QAAQ,KAAK,YAAe,UAAU,KAAK,OAAO,CAAC,CAAC;CACnE;CAGA,QAAQ,UAAkB,SAA2B;EACnD,OAAO,KAAK,KAAK,IAAI,KAAK,IAAI,QAAQ,GAAG;GACvC,GAAG,KAAK,YAAY,OAAO;GAC3B,cAAc;EACf,CAAA;CACH;CAGA,QAAQ,UAAkB,SAA2B;EACnD,OAAO,KAAK,KAAK,IAAI,KAAK,IAAI,QAAQ,GAAG;GACvC,GAAG,KAAK,YAAY,OAAO;GAC3B,cAAc;EACf,CAAA;CACH;CAQA,YAAe,UAAkB,SAA2B;EAC1D,MAAM,WAAW,KAAK,gBAAgB,OAAO;EAC7C,OAAO,KAAK,KACT,IAAO,KAAK,IAAI,QAAQ,GAAG;GAC1B,GAAG,KAAK,YAAY,OAAO;GAC3B,SAAS;EACV,CAAA,CAAC,CACD,KACC,KAAK,QAAO;GACV,IAAI,UAAU,eAAe,IAAI,MAAM,QAAQ;GAC/C,OAAO;EACT,CAAC,CAAC;CAER;CAMA,aACE,UACA,MACA,SAA2B;EAE3B,MAAM,WAAW,KAAK,gBAAgB,OAAO;EAC7C,OAAO,KAAK,KACT,KAAQ,KAAK,IAAI,QAAQ,GAAG,MAAM;GACjC,GAAG,KAAK,YAAY,OAAO;GAC3B,SAAS;EACV,CAAA,CAAC,CACD,KACC,KAAK,QAAO;GACV,IAAI,UAAU,eAAe,IAAI,MAAM,QAAQ;GAC/C,OAAO;EACT,CAAC,CAAC;CAER;CASA,YACE,UACA,SAAmC;EAEnC,MAAM,EAAE,cAAc,OAAO,gBAAgB,GAAG,mBAAmB,WAAW,CAAA;EAC9E,MAAM,SACJ,OAAO,aAAa,cAAc,CAAC,SAAS,QAAQ,IAChD,SAAS,QAAQ,IAChB;EACP,MAAM,gBAAe;GACnB,MAAM,KAAK,OAAO,WAAW,WAAW,SAAS,OAAM;GACvD,IAAI,OAAO,QAAW,OAAO;GAC7B,OAAO;IACL,KAAK,KAAK,IAAI,EAAE;IAChB,QAAQ,KAAK,SAAS,aAAa,eAAe,MAAM,CAAC;IACzD,SAAS,aAAa,eAAe,OAAO;IAC5C,SAAS,aAAa,eAAe,OAAO;IAC5C,iBAAiB,eAAe;IAChC,GAAI,mBAAmB,SAAY,EAAE,eAAc,IAAK,CAAA;GACzD;EACH;EACA,MAAM,WAAW,KAAK,gBAAgB,cAAc;EACpD,MAAM,OAAO,EAAgB,aAA6B;EAG1D,MAAM,cAAc,QAAmB;GACrC,IAAI,UAAU,eAAe,KAAK,QAAQ;GAC1C,IAAI,eAAe,WAAW,MAAM,OAAO,QAAQ,MAAM,GAAQ,IAAK;GACtE,MAAM,YAAY,WAAW,KAAK,eAAe,SAAS;GAC1D,OAAO,QAAQ,MAAM,SAAS,IAAI;EACpC;EACA,OAAO,iBAAiB,SACpB,aAAgB,SAAS;GAAE,GAAG;GAAM,OAAO;EAAU,CAAE,IACvD,aAAgB,SAAS;GACvB,GAAG;GACW;GACd,OAAO;EACR,CAAA;CACP;CAEQ,IAAI,UAAgB;EAG1B,OAAO;CACT;CAEQ,gBAAgB,SAEvB;EACC,OAAO,SAAS,YAAY,KAAK,QAAQ;CAC3C;CAEQ,YAAe,UAAkB,MAAe,SAA+B;EACrF,MAAM,WAAW,KAAK,gBAAgB,OAAO;EAC7C,IAAI,UAAU,eAAe,MAAM,QAAQ;EAC3C,IAAI,SAAS,WAAW,MAAM,OAAO;EACrC,OAAO,WAAW,MAAM,QAAQ,WAAW,QAAQ;CACrD;CAEQ,YAAY,SAA4B;EAC9C,OAAO;GACL,QAAQ,KAAK,SAAS,SAAS,MAAM;GACrC,SAAS,SAAS;GAClB,SAAS,SAAS,WAAW,IAAI,YAAW;GAC5C,iBAAiB,SAAS;EAC3B;CACH;CAEQ,SAAS,OAAwB;EACvC,IAAI,CAAC,OAAO,OAAO;EACnB,IAAI,iBAAiB,YAAY,OAAO;EACxC,IAAI,SAAS,IAAI,WAAU;EAC3B,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,GAC7C,IAAI,UAAU,UAAa,UAAU,MACnC,SAAS,OAAO,IAAI,KAAK,OAAO,KAAK,CAAC;EAG1C,OAAO;CACT;CAxLW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAW,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAX,OAAA,QAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;CAAW,CAAA;;AAAX,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAW,YAAA,CAAA,EADvB,MAAA,QAAA,CAAA;;AAgMD,SAAS,aAAgB,OAAmC;CAC1D,IAAI,UAAU,QAAW,OAAO;CAChC,IAAI,SAAS,KAAK,GAAG,OAAQ,MAAmB;CAChD,IAAI,OAAO,UAAU,YAAY,OAAQ,MAAiB;CAC1D,OAAO;AACT;ACzPA,SAAgB,mBAAmB,SAA4B,CAAA,GAAE;CAC/D,MAAM,EAAE,cAAc,GAAG,SAAS;CAClC,OAAO,yBAAyB,CAC9B,GAAI,eAAe,CAAC,kBAAkB,GAAG,YAAY,CAAC,IAAI,CAAA,GAC1D;EAAE,SAAS;EAAqB,UAAU;CAAI,CAC/C,CAAA;AACH;AAGA,SAAgB,oBAAiB;CAC/B,OAAO,OAAO,WAAW;AAC3B"}
1
+ {"version":3,"file":"rivinia-http.mjs","names":[],"sources":["../../../projects/http/src/lib/utils/http-utils.ts","../../../projects/http/src/lib/models/http-envelope.ts","../../../projects/http/src/lib/models/http-config.ts","../../../projects/http/src/lib/services/rivinia-http.ts","../../../projects/http/src/lib/providers/provide-http.ts"],"sourcesContent":["/**\n * Joins a base URL and an endpoint without producing double slashes.\n * Absolute `http(s)` endpoints pass through untouched.\n */\nexport function joinUrl(baseUrl: string, endpoint: string): string {\n if (/^https?:\\/\\//i.test(endpoint)) {\n return endpoint;\n }\n const base = baseUrl.replace(/\\/+$/, '');\n const path = endpoint.replace(/^\\/+/, '');\n return path ? `${base}/${path}` : base;\n}\n\n/**\n * Typed extraction failure. Thrown when explicit unwrapping\n * (`{ unwrap: true, unwrapKey }`) cannot be performed: empty key, non-object\n * body, or missing property. Never thrown when unwrapping is disabled.\n * In `getResource` this surfaces as `resource.error()`.\n */\nexport class RiviniaHttpUnwrapError extends Error {\n /** The requested envelope key. */\n readonly key: string;\n /** The endpoint being read, when known (never the body). */\n readonly endpoint: string | undefined;\n /**\n * The raw response body. Attached for diagnosis (a `parse` hook or logger\n * can inspect it) — it may carry PII, so never log it blindly and never\n * interpolate it into messages.\n */\n readonly body: unknown;\n\n constructor(key: string, body: unknown, endpoint?: string, reason?: string) {\n super(\n `[rivinia/http] Cannot unwrap key \"${key}\"` +\n (endpoint ? ` from ${endpoint}` : '') +\n (reason ? `: ${reason}` : ': property missing'),\n );\n this.name = 'RiviniaHttpUnwrapError';\n this.key = key;\n this.endpoint = endpoint;\n this.body = body;\n }\n}\n\n/**\n * Strict single-level extraction shared by every unwrapping path (plain\n * methods + `getResource`). Rules:\n * - the key must be a non-empty string (an empty key is never intentional);\n * - the body must be a non-null, non-array object;\n * - presence is an OWN-property check (`hasOwnProperty`) — inherited\n * properties (`'toString' in {}`) never count;\n * - a present-but-falsy value (`null`, `undefined`, `false`, `0`, `''`) is\n * delivered unchanged — truthiness never decides success;\n * - anything else throws `RiviniaHttpUnwrapError`. The original body is\n * never silently returned after an explicit extraction fails.\n *\n * Limit, stated openly: presence proves nothing about the value matching an\n * arbitrary generic `T`. Pair with `envelope` validation or a `parse` hook\n * (Zod/Valibot) when the backend is untyped.\n */\nexport function extractKey(body: unknown, key: string, endpoint?: string): unknown {\n if (typeof key !== 'string' || key.length === 0) {\n throw new RiviniaHttpUnwrapError(key, body, endpoint, 'invalid key');\n }\n if (typeof body !== 'object' || body === null || Array.isArray(body)) {\n throw new RiviniaHttpUnwrapError(key, body, endpoint, 'response is not an object');\n }\n const record = body as Record<string, unknown>;\n if (!Object.prototype.hasOwnProperty.call(record, key)) {\n throw new RiviniaHttpUnwrapError(key, body, endpoint);\n }\n return record[key];\n}\n\n/**\n * Extracts `body[key]` when the backend wraps payloads in an envelope\n * (e.g. `{ data, error, isSuccess }`). Strict: a missing key, a non-object\n * body, or an empty key throws `RiviniaHttpUnwrapError` — the original body\n * is never silently passed through after an explicit extraction request.\n * Unwraps EXACTLY ONE level: a payload that itself contains the key\n * (`{ data: { data: […], page: 1 } }`) is returned whole.\n */\nexport function unwrapAs<T>(body: unknown, key: string): T {\n return extractKey(body, key) as T;\n}\n","/**\n * Typed envelope failure. Thrown when a backend signals failure inside a\n * successful HTTP response (e.g. `{ data: null, isSuccess: false }` with\n * HTTP 200) and the envelope policy opts into throwing. In `getResource`\n * this surfaces as `resource.error()`.\n */\nexport class RiviniaHttpEnvelopeError extends Error {\n /** The raw response body. */\n readonly body: unknown;\n /** The machine-readable failure code (`body[errorKey]`), if any. */\n readonly code: unknown;\n\n constructor(body: unknown, code: unknown) {\n super(\n typeof code === 'string' && code.length > 0\n ? `[rivinia/http] Request failed: ${code}`\n : '[rivinia/http] Request failed (backend envelope)',\n );\n this.name = 'RiviniaHttpEnvelopeError';\n this.body = body;\n this.code = code;\n }\n}\n\n/**\n * Opt-in backend-envelope validation. When present (globally or per call),\n * a response object whose `successKey` is strictly `false` throws\n * `RiviniaHttpEnvelopeError` instead of flowing into `next()` as success.\n * Runs on the raw body before unwrapping, independent of `unwrap`.\n */\nexport interface RiviniaEnvelopePolicy {\n /** Body key holding the success flag. @default 'isSuccess' */\n readonly successKey?: string;\n /** Body key holding the failure code. @default 'error' */\n readonly errorKey?: string;\n /** Throw on failure. Explicit `false` disables (policy becomes passive). @default true */\n readonly throwOnFailure?: boolean;\n}\n\nconst DEFAULT_SUCCESS_KEY = 'isSuccess';\nconst DEFAULT_ERROR_KEY = 'error';\n\n/** Throws when the envelope signals failure and the policy opts in. */\nexport function assertEnvelope(body: unknown, policy: RiviniaEnvelopePolicy): void {\n if (policy.throwOnFailure === false) return;\n if (typeof body !== 'object' || body === null || Array.isArray(body)) return;\n const successKey = policy.successKey ?? DEFAULT_SUCCESS_KEY;\n const record = body as Record<string, unknown>;\n if (!(successKey in record) || record[successKey] !== false) return;\n const errorKey = policy.errorKey ?? DEFAULT_ERROR_KEY;\n throw new RiviniaHttpEnvelopeError(body, record[errorKey]);\n}\n","import type { HttpFeature, HttpFeatureKind } from '@angular/common/http';\nimport { InjectionToken } from '@angular/core';\nimport type { RiviniaEnvelopePolicy } from './http-envelope';\n\n/**\n * Global configuration for `@rivinia/http`.\n *\n * Unwrapping is strictly per request (`{ unwrap: true, unwrapKey }` on the\n * call) — there is intentionally no global unwrap configuration. A global\n * key would silently change what every call returns; an explicit key keeps\n * each call honest. URLs are ALWAYS used as given — the library never joins,\n * prefixes, or rewrites them. Build them at the call site from wherever you\n * want (`environment.apiUrl`, a constant, a signal …) with the exported\n * `joinUrl`.\n */\nexport interface RiviniaHttpConfig {\n /**\n * Optional `HttpClient` features (`withInterceptors(...)`, `withXhr()`, …),\n * applied via a single internal `provideHttpClient(...httpFeatures)` call.\n *\n * Use this INSTEAD of calling `provideHttpClient()` yourself — calling both\n * registers the built-in XSRF interceptor twice (verified against Angular\n * 22.2.2 source: `xsrfInterceptorFn` is a multi-provider with no cross-call\n * dedup). Omit it entirely on Angular ≥21 when you need no features:\n * `HttpClient` (fetch backend) is injectable by default.\n */\n httpFeatures?: HttpFeature<HttpFeatureKind>[];\n /**\n * Opt-in backend-envelope validation applied to every call unless the call\n * provides its own `envelope` (which replaces this wholesale). See\n * `RiviniaEnvelopePolicy`. Off by default — plain bodies flow untouched.\n */\n envelope?: RiviniaEnvelopePolicy;\n}\n\n/**\n * Internal configuration token. Consumers must use `provideRiviniaHttp()`.\n * @internal\n */\nexport const RIVINIA_HTTP_CONFIG = new InjectionToken<RiviniaHttpConfig>('RIVINIA_HTTP_CONFIG');\n","import {\n HttpClient,\n HttpContext,\n HttpParams,\n httpResource,\n type HttpHeaders,\n type HttpResourceRef,\n type HttpResponse,\n} from '@angular/common/http';\nimport { Service, computed, inject, isSignal, type Signal } from '@angular/core';\nimport { map, type Observable } from 'rxjs';\nimport { RIVINIA_HTTP_CONFIG, type RiviniaHttpConfig } from '../models/http-config';\nimport type {\n QueryParamsInput,\n ReactiveInput,\n RiviniaDeleteRequestOptions,\n RiviniaRawOptions,\n RiviniaRequestBase,\n RiviniaRequestOptions,\n RiviniaUnwrapOff,\n RiviniaUnwrapOn,\n} from '../models/http-options';\nimport { extractKey } from '../utils/http-utils';\nimport { assertEnvelope, type RiviniaEnvelopePolicy } from '../models/http-envelope';\n\n/**\n * Reactive per-request fields for `getResource` (re-evaluated on every\n * request; everything else is read once per call site).\n */\nexport interface RiviniaResourceBase<T> {\n /**\n * Query params re-evaluated on every request: value, signal, or reactive\n * function. Anything else in these options is read once per call site —\n * only the URL, params, headers and context are reactive.\n */\n params?: ReactiveInput<QueryParamsInput>;\n headers?: ReactiveInput<HttpHeaders | Record<string, string | string[]>>;\n context?: ReactiveInput<HttpContext>;\n withCredentials?: boolean;\n /** Value while idle/loading. Defaults to `undefined`. */\n defaultValue?: T;\n /**\n * Runs AFTER envelope unwrapping: `raw -> unwrap -> parse`.\n * Use for runtime validation (e.g. Zod) or shaping.\n */\n parse?: (value: unknown) => T;\n /**\n * Forwarded to the `httpResource` request object only (enables the\n * resource `progress` signal for downloads). Has no effect on the\n * plain Observable methods — use `HttpClient` events there directly.\n */\n reportProgress?: boolean;\n /**\n * Opt-in backend-envelope validation for this resource (replaces any\n * global `envelope` policy wholesale). See `RiviniaEnvelopePolicy`.\n */\n envelope?: RiviniaEnvelopePolicy;\n}\n\n/**\n * `getResource` options: reactive base + the same per-call unwrap contract\n * as the plain methods (composed explicitly — `Omit` over a union would\n * destroy the discrimination).\n */\nexport type RiviniaResourceOptions<T> = RiviniaResourceBase<T> &\n (RiviniaUnwrapOn | RiviniaUnwrapOff);\n\n/**\n * Headless typed HTTP client.\n *\n * Thin over `HttpClient` (so app interceptors, `HttpContext` escape hatches\n * and `provideHttpClient()` testing all keep working). Owns no backend:\n * URLs pass through untouched; the library adds opt-in envelope handling —\n * per-request unwrapping (`{ unwrap: true, unwrapKey }`, mandatory key, no\n * default) and failure validation (`envelope`).\n */\n@Service()\nexport class RiviniaHttp {\n private readonly http = inject(HttpClient);\n private readonly config = inject<RiviniaHttpConfig>(RIVINIA_HTTP_CONFIG, {\n optional: true,\n });\n\n get<T>(endpoint: string, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .get<unknown>(this.url(endpoint), this.httpOptions(options))\n .pipe(map((body) => this.applyUnwrap<T>(endpoint, body, options)));\n }\n\n post<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .post<unknown>(this.url(endpoint), body, this.httpOptions(options))\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n put<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .put<unknown>(this.url(endpoint), body, this.httpOptions(options))\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n patch<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .patch<unknown>(this.url(endpoint), body, this.httpOptions(options))\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n delete<T>(endpoint: string, options?: RiviniaDeleteRequestOptions): Observable<T> {\n return this.http\n .delete<unknown>(this.url(endpoint), {\n ...this.httpOptions(options),\n body: options?.body,\n })\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n /** Binary download. Never unwrapped — envelopes don't apply to blobs. */\n getBlob(endpoint: string, options?: RiviniaRawOptions): Observable<Blob> {\n return this.http.get(this.url(endpoint), {\n ...this.httpOptions(options),\n responseType: 'blob',\n });\n }\n\n /** Plain-text download. Never unwrapped. */\n getText(endpoint: string, options?: RiviniaRawOptions): Observable<string> {\n return this.http.get(this.url(endpoint), {\n ...this.httpOptions(options),\n responseType: 'text',\n });\n }\n\n /**\n * Full response access (status, headers — e.g. pagination counts or a 201\n * `Location`). Raw body (no unwrapping), but the configured/per-call\n * `envelope` validation still applies to the body — a globally declared\n * failure policy must hold on every JSON path, not just the unwrapped ones.\n */\n getResponse<T>(endpoint: string, options?: RiviniaRawOptions): Observable<HttpResponse<T>> {\n const envelope = this.resolveEnvelope(options);\n return this.http\n .get<T>(this.url(endpoint), {\n ...this.httpOptions(options),\n observe: 'response',\n })\n .pipe(\n map((res) => {\n if (envelope) assertEnvelope(res.body, envelope);\n return res;\n }),\n );\n }\n\n /**\n * Full response access for creates (status, `Location` header). Same\n * envelope-validation rule as `getResponse`; body otherwise raw.\n */\n postResponse<T>(\n endpoint: string,\n body?: unknown,\n options?: RiviniaRawOptions,\n ): Observable<HttpResponse<T>> {\n const envelope = this.resolveEnvelope(options);\n return this.http\n .post<T>(this.url(endpoint), body, {\n ...this.httpOptions(options),\n observe: 'response',\n })\n .pipe(\n map((res) => {\n if (envelope) assertEnvelope(res.body, envelope);\n return res;\n }),\n );\n }\n\n /**\n * Reactive GET as an `httpResource` (stable since Angular v22).\n * The endpoint can be a static string, a signal, or a reactive function\n * (same as `httpResource` itself) — it re-fetches on change, and\n * `undefined` disables the request. Unwrap applies through `parse`,\n * composed before a user `parse` if given.\n */\n getResource<T>(\n endpoint: string | Signal<string | undefined> | (() => string | undefined),\n options?: RiviniaResourceOptions<T>,\n ): HttpResourceRef<T | undefined> {\n const { defaultValue, parse, reportProgress, ...requestOptions } = options ?? {};\n const source: string | Signal<string | undefined> =\n typeof endpoint === 'function' && !isSignal(endpoint)\n ? computed(endpoint)\n : (endpoint as string | Signal<string | undefined>);\n const request = () => {\n const ep = typeof source === 'string' ? source : source();\n if (ep === undefined) return undefined;\n return {\n url: this.url(ep),\n params: this.toParams(resolveInput(requestOptions.params)),\n headers: resolveInput(requestOptions.headers),\n context: resolveInput(requestOptions.context),\n withCredentials: requestOptions.withCredentials,\n ...(reportProgress !== undefined ? { reportProgress } : {}),\n };\n };\n const envelope = this.resolveEnvelope(requestOptions);\n const base = { defaultValue: defaultValue as T | undefined };\n // Pipeline order: envelope validation (raw) -> unwrap -> user parse.\n // A throwing envelope or a failed extraction surfaces as resource.error().\n const applyParse = (raw: unknown): T => {\n if (envelope) assertEnvelope(raw, envelope);\n if (requestOptions.unwrap !== true) return parse ? parse(raw as T) : (raw as T);\n const unwrapped = extractKey(raw, requestOptions.unwrapKey) as T;\n return parse ? parse(unwrapped) : unwrapped;\n };\n return defaultValue === undefined\n ? httpResource<T>(request, { ...base, parse: applyParse })\n : httpResource<T>(request, {\n ...base,\n defaultValue: defaultValue as T,\n parse: applyParse,\n });\n }\n\n private url(endpoint: string): string {\n // Deliberate passthrough: the caller owns the URL (environment, constant,\n // signal …). Use the exported `joinUrl` at the call site when composing.\n return endpoint;\n }\n\n private resolveEnvelope(options?: {\n envelope?: RiviniaEnvelopePolicy;\n }): RiviniaEnvelopePolicy | undefined {\n return options?.envelope ?? this.config?.envelope;\n }\n\n private applyUnwrap<T>(endpoint: string, body: unknown, options?: RiviniaRequestOptions): T {\n const envelope = this.resolveEnvelope(options);\n if (envelope) assertEnvelope(body, envelope);\n if (options?.unwrap !== true) return body as T;\n return extractKey(body, options.unwrapKey, endpoint) as T;\n }\n\n private httpOptions(options?: RiviniaRequestBase) {\n return {\n params: this.toParams(options?.params),\n headers: options?.headers,\n context: options?.context ?? new HttpContext(),\n withCredentials: options?.withCredentials,\n };\n }\n\n private toParams(input?: QueryParamsInput): HttpParams | undefined {\n if (!input) return undefined;\n if (input instanceof HttpParams) return input;\n let params = new HttpParams();\n for (const [key, value] of Object.entries(input)) {\n if (value !== undefined && value !== null) {\n params = params.set(key, String(value));\n }\n }\n return params;\n }\n}\n\n/**\n * Evaluates a reactive input inside the request function: signals are read\n * (tracked), plain functions are called, values pass through.\n */\nfunction resolveInput<T>(value: ReactiveInput<T> | undefined): T | undefined {\n if (value === undefined) return undefined;\n if (isSignal(value)) return (value as Signal<T>)();\n if (typeof value === 'function') return (value as () => T)();\n return value as T;\n}\n","import { EnvironmentProviders, inject, makeEnvironmentProviders } from '@angular/core';\nimport { provideHttpClient } from '@angular/common/http';\nimport { RIVINIA_HTTP_CONFIG, type RiviniaHttpConfig } from '../models/http-config';\nimport { RiviniaHttp } from '../services/rivinia-http';\n\n/**\n * Configures `@rivinia/http`.\n *\n * URLs are always used as given — build them at the call site from wherever\n * you want (see the exported `joinUrl` + your `environment.apiUrl`).\n *\n * Three backend styles (pick ONE):\n * - `provideRiviniaHttp()` alone — Angular ≥21 already injects `HttpClient` (fetch).\n * - `provideRiviniaHttp({ httpFeatures: [withInterceptors([...])] })` — single\n * call that also configures the backend. Do NOT additionally call\n * `provideHttpClient()` (double registration duplicates the XSRF interceptor).\n * - `provideHttpClient(...)` + `provideRiviniaHttp()` separately — equivalent,\n * keeps backend config visibly in app code (this is what `ng add` generates).\n *\n * @example\n * ```ts\n * provideRiviniaHttp()\n * ```\n */\nexport function provideRiviniaHttp(config: RiviniaHttpConfig = {}): EnvironmentProviders {\n const { httpFeatures, ...rest } = config;\n return makeEnvironmentProviders([\n ...(httpFeatures ? [provideHttpClient(...httpFeatures)] : []),\n { provide: RIVINIA_HTTP_CONFIG, useValue: rest },\n ]);\n}\n\n/** Shorthand for `inject(RiviniaHttp)`. */\nexport function injectRiviniaHttp(): RiviniaHttp {\n return inject(RiviniaHttp);\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rivinia/http",
3
- "version": "0.5.3",
3
+ "version": "1.0.0",
4
4
  "description": "Headless typed HTTP client for Angular: envelope unwrapping, plain Observables and httpResource.",
5
5
  "keywords": [
6
6
  "angular",
@@ -22,6 +22,18 @@
22
22
  },
23
23
  "sideEffects": false,
24
24
  "schematics": "./schematics/collection.json",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/abdelfattahqandil21-oss/Revinia.git",
28
+ "directory": "projects/http"
29
+ },
30
+ "homepage": "https://github.com/abdelfattahqandil21-oss/Revinia/tree/http/v1.0.0/projects/http#readme",
31
+ "bugs": {
32
+ "url": "https://github.com/abdelfattahqandil21-oss/Revinia/issues"
33
+ },
34
+ "engines": {
35
+ "node": "^22.22.3 || ^24.15.0 || >=26.0.0"
36
+ },
25
37
  "module": "fesm2022/rivinia-http.mjs",
26
38
  "typings": "types/rivinia-http.d.ts",
27
39
  "exports": {
@@ -1,6 +1,6 @@
1
1
  import { HttpContext, HttpFeature, HttpFeatureKind, HttpHeaders, HttpParams, HttpResourceRef, HttpResponse } from "@angular/common/http";
2
2
  import * as i0 from "@angular/core";
3
- import { EnvironmentProviders, InjectionToken, Signal } from "@angular/core";
3
+ import { EnvironmentProviders, Signal } from "@angular/core";
4
4
  import { Observable } from "rxjs";
5
5
  /**
6
6
  * Typed envelope failure. Thrown when a backend signals failure inside a
@@ -29,8 +29,6 @@ interface RiviniaEnvelopePolicy {
29
29
  /** Throw on failure. Explicit `false` disables (policy becomes passive). @default true */
30
30
  readonly throwOnFailure?: boolean;
31
31
  }
32
- /** Throws when the envelope signals failure and the policy opts in. */
33
- declare function assertEnvelope(body: unknown, policy: RiviniaEnvelopePolicy): void;
34
32
  /**
35
33
  * Global configuration for `@rivinia/http`.
36
34
  *
@@ -61,11 +59,6 @@ interface RiviniaHttpConfig {
61
59
  */
62
60
  envelope?: RiviniaEnvelopePolicy;
63
61
  }
64
- /**
65
- * Internal configuration token. Consumers must use `provideRiviniaHttp()`.
66
- * @internal
67
- */
68
- declare const RIVINIA_HTTP_CONFIG: InjectionToken<RiviniaHttpConfig>;
69
62
  /**
70
63
  * A value, a signal of it, or a reactive function returning it — evaluated
71
64
  * inside the `httpResource` request function, so signal reads stay reactive