@fixback/sdk-core 0.2.0 → 0.3.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.
Files changed (68) hide show
  1. package/README.md +28 -9
  2. package/{src/annotation.ts → dist/annotation.d.ts} +36 -47
  3. package/dist/anonymous-id.d.ts +23 -0
  4. package/dist/anonymous-id.js +39 -0
  5. package/dist/anonymous-id.js.map +1 -0
  6. package/dist/auto-capture.d.ts +129 -0
  7. package/dist/auto-capture.js +210 -0
  8. package/dist/auto-capture.js.map +1 -0
  9. package/{src/backoff.ts → dist/backoff.d.ts} +17 -52
  10. package/dist/boot.d.ts +148 -0
  11. package/dist/boot.js +113 -0
  12. package/dist/boot.js.map +1 -0
  13. package/dist/breadcrumb.d.ts +133 -0
  14. package/dist/connect.d.ts +110 -0
  15. package/dist/connect.js +147 -0
  16. package/dist/connect.js.map +1 -0
  17. package/dist/env.d.ts +37 -0
  18. package/dist/env.js +83 -0
  19. package/dist/env.js.map +1 -0
  20. package/dist/fingerprint.d.ts +39 -0
  21. package/dist/fingerprint.js +10 -15
  22. package/dist/fingerprint.js.map +1 -1
  23. package/dist/http.d.ts +51 -0
  24. package/dist/http.js +42 -0
  25. package/dist/http.js.map +1 -0
  26. package/dist/index.d.ts +28 -0
  27. package/dist/index.js +115 -3
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +1057 -0
  30. package/dist/index.mjs.map +7 -0
  31. package/dist/options.d.ts +82 -0
  32. package/dist/options.js +22 -0
  33. package/dist/options.js.map +1 -0
  34. package/dist/release.d.ts +19 -0
  35. package/dist/release.js +36 -0
  36. package/dist/release.js.map +1 -0
  37. package/dist/scrub.d.ts +62 -0
  38. package/dist/stack.d.ts +51 -0
  39. package/dist/stack.js +97 -0
  40. package/dist/stack.js.map +1 -0
  41. package/dist/trace/buffer.d.ts +121 -0
  42. package/dist/trace/buffer.js +230 -0
  43. package/dist/trace/buffer.js.map +1 -0
  44. package/dist/trace/console-args.d.ts +60 -0
  45. package/dist/trace/console-args.js +189 -0
  46. package/dist/trace/console-args.js.map +1 -0
  47. package/dist/trace/console.d.ts +42 -0
  48. package/dist/trace/console.js +71 -0
  49. package/dist/trace/console.js.map +1 -0
  50. package/dist/trace/crumbs.d.ts +88 -0
  51. package/dist/trace/crumbs.js +164 -0
  52. package/dist/trace/crumbs.js.map +1 -0
  53. package/dist/trace/source.d.ts +30 -0
  54. package/dist/trace/source.js +59 -0
  55. package/dist/trace/source.js.map +1 -0
  56. package/dist/version.d.ts +13 -0
  57. package/dist/version.js +17 -0
  58. package/dist/version.js.map +1 -0
  59. package/dist/wire.d.ts +101 -0
  60. package/package.json +12 -8
  61. package/src/backoff.test.ts +0 -94
  62. package/src/breadcrumb.ts +0 -169
  63. package/src/fingerprint.test.ts +0 -96
  64. package/src/fingerprint.ts +0 -112
  65. package/src/index.ts +0 -63
  66. package/src/scrub.test.ts +0 -215
  67. package/src/scrub.ts +0 -226
  68. package/src/wire.ts +0 -116
@@ -12,13 +12,10 @@
12
12
  * The clock is injectable so the window is unit-tested deterministically, never on
13
13
  * wall time — mirroring the server limiter's `Clock`.
14
14
  */
15
-
16
15
  /** A source of the current time in epoch milliseconds — injectable for tests. */
17
16
  export type Clock = () => number;
18
-
19
17
  /** The hold window applied when a `429` carries no usable `Retry-After` (spec §E). */
20
- export const DEFAULT_RETRY_AFTER_SECONDS = 60;
21
-
18
+ export declare const DEFAULT_RETRY_AFTER_SECONDS = 60;
22
19
  /**
23
20
  * Parse a `Retry-After` header into whole seconds to hold for. Handles both HTTP
24
21
  * forms — a delta-seconds integer and an HTTP-date (measured from `now`, rounded up
@@ -26,58 +23,26 @@ export const DEFAULT_RETRY_AFTER_SECONDS = 60;
26
23
  * the header is absent, blank, or unparseable. Ingest sends the delta-seconds form;
27
24
  * the date form is handled for spec-completeness.
28
25
  */
29
- export function parseRetryAfter(
30
- header: string | null | undefined,
31
- now: number,
32
- ): number {
33
- if (header == null) return DEFAULT_RETRY_AFTER_SECONDS;
34
- const value = header.trim();
35
- if (value === "") return DEFAULT_RETRY_AFTER_SECONDS;
36
-
37
- if (/^\d+$/.test(value)) {
38
- return Number(value);
39
- }
40
-
41
- const when = Date.parse(value);
42
- if (!Number.isNaN(when)) {
43
- return Math.max(0, Math.ceil((when - now) / 1000));
44
- }
45
-
46
- return DEFAULT_RETRY_AFTER_SECONDS;
47
- }
48
-
26
+ export declare function parseRetryAfter(header: string | null | undefined, now: number): number;
49
27
  /**
50
28
  * A single pause window for automatic (`source: error`) reports. `hold` opens (or
51
29
  * extends) it from a `429`'s `Retry-After`; `isPaused` reports whether it is still
52
30
  * open. The default instance in the SDK's transport is shared across a page's
53
31
  * reports so the hold persists across successive automatic submissions.
54
32
  */
55
- export class AutoReportBackoff {
56
- /** Epoch ms until which automatic reports are held; `0` when clear. */
57
- private pausedUntil = 0;
58
-
59
- constructor(private readonly now: Clock = Date.now) {}
60
-
61
- /** Is the automatic-report pause window currently open? */
62
- isPaused(): boolean {
63
- return this.now() < this.pausedUntil;
64
- }
65
-
66
- /** Whole seconds remaining in the pause window (`0` when clear). */
67
- retryAfterSeconds(): number {
68
- return Math.max(0, Math.ceil((this.pausedUntil - this.now()) / 1000));
69
- }
70
-
71
- /**
72
- * Open (or extend) the window from a `429`'s `Retry-After` value, returning the
73
- * seconds it will hold for. The window only ever grows — a shorter later hold
74
- * never clips a longer one already in effect.
75
- */
76
- hold(retryAfterHeader: string | null | undefined): number {
77
- const now = this.now();
78
- const seconds = parseRetryAfter(retryAfterHeader, now);
79
- const until = now + seconds * 1000;
80
- if (until > this.pausedUntil) this.pausedUntil = until;
81
- return seconds;
82
- }
33
+ export declare class AutoReportBackoff {
34
+ private readonly now;
35
+ /** Epoch ms until which automatic reports are held; `0` when clear. */
36
+ private pausedUntil;
37
+ constructor(now?: Clock);
38
+ /** Is the automatic-report pause window currently open? */
39
+ isPaused(): boolean;
40
+ /** Whole seconds remaining in the pause window (`0` when clear). */
41
+ retryAfterSeconds(): number;
42
+ /**
43
+ * Open (or extend) the window from a `429`'s `Retry-After` value, returning the
44
+ * seconds it will hold for. The window only ever grows — a shorter later hold
45
+ * never clips a longer one already in effect.
46
+ */
47
+ hold(retryAfterHeader: string | null | undefined): number;
83
48
  }
package/dist/boot.d.ts ADDED
@@ -0,0 +1,148 @@
1
+ /**
2
+ * The ingest **boot + feedback wire contract** — the one copy every capture SDK
3
+ * speaks (ADR-0028).
4
+ *
5
+ * The SDKs deliberately do not import `@fixback/shared` — that package is private
6
+ * and server-shaped (ticket #47) — so the exact slice they need lives here, in the
7
+ * shared core, rather than being vendored once per SDK. Keep it in lock-step with
8
+ * the server: the request body accepted by `POST /api/ingest/boot`
9
+ * (`apps/api/src/ingest/ingest.controller.ts`) and the `BootAnswer` returned by
10
+ * `evaluateBoot` (`apps/api/src/ingest/reporter-identity.ts`).
11
+ *
12
+ * One runtime difference is carried as an option, not a fork: a browser attaches
13
+ * the `Origin` header itself, while a native HTTP stack attaches none — so React
14
+ * Native passes its configured `origin` and the server reads it to decide
15
+ * `originAllowed` exactly as it does for a browser request (spec 0004 §A).
16
+ */
17
+ import { type FetchLike } from "./http";
18
+ /** A Project's Gate — who may submit. Mirrors the server's `ProjectGate`. */
19
+ export type ProjectGate = "open" | "invited" | "internal";
20
+ /** The trust tier a Reporter holds. Mirrors the server's `ReporterTier`. */
21
+ export type ReporterTier = "public" | "invited" | "internal";
22
+ /**
23
+ * Optional identity evidence an SDK forwards to boot. None of it is a tier: the
24
+ * server re-derives trust from this evidence and never honours a self-declared
25
+ * tier, so no SDK sends one.
26
+ */
27
+ export interface IdentityInputs {
28
+ /** A Host identity token minted by the customer's server (the escape hatch). */
29
+ readonly hostIdentity?: string;
30
+ /**
31
+ * The **Reporter session** token the SDK holds after a Connect (ADR-0032). The
32
+ * server checks it against the live session row and re-derives the Account's tier;
33
+ * a revoked, expired, or forged token is treated as absent (the anonymous branch),
34
+ * never an error. Replaces the removed device-local `reporterId` branch.
35
+ */
36
+ readonly reporterSession?: string;
37
+ /** The anonymous first-party identifier for a Public Reporter on an Open Project. */
38
+ readonly anonymousId?: string;
39
+ }
40
+ /** The JSON body `POST /api/ingest/boot` accepts. */
41
+ export interface BootRequest extends IdentityInputs {
42
+ readonly key: string;
43
+ }
44
+ /**
45
+ * The signed-in Account a valid Reporter session resolves to, as the boot answer and
46
+ * the Connect exchange report it for the identity chip (ADR-0032). Name and email
47
+ * come from the `user`, never from anything the client set. `null`/absent means the
48
+ * caller is anonymous.
49
+ */
50
+ export interface ConnectedAccount {
51
+ readonly name: string;
52
+ readonly email: string;
53
+ }
54
+ /**
55
+ * The Project's effective console/network capture config, served on the boot answer
56
+ * (spec #122 §L; ticket #138). Each flag is the per-project master toggle ANDed with
57
+ * that stream's own toggle, so an SDK gates instrumentation on one boolean per
58
+ * stream. Optional on the wire: an older server that does not send it (or a
59
+ * malformed value) is treated as **capture on** — default-on, matching the server
60
+ * default — and init options override whatever is served.
61
+ */
62
+ export interface CaptureConfig {
63
+ readonly console: boolean;
64
+ readonly network: boolean;
65
+ /**
66
+ * The Project's session-replay toggle (issue #189, ADR-0024) — browser only.
67
+ * Optional on the wire: a server predating replay omits it, which — like the
68
+ * other streams — means **capture on**.
69
+ */
70
+ readonly replay?: boolean;
71
+ }
72
+ /**
73
+ * The boot answer: whether this origin is allowlisted, the Project's Gate, the
74
+ * caller's derived tier (`null` when a presented identity was refused), whether a
75
+ * submission would be accepted right now, and the Project's capture config. An SDK
76
+ * shows its launcher (or arms its gesture) only when `canSubmit` is true.
77
+ */
78
+ export interface BootAnswer {
79
+ readonly originAllowed: boolean;
80
+ readonly gate: ProjectGate;
81
+ readonly tier: ReporterTier | null;
82
+ readonly canSubmit: boolean;
83
+ /** The Project's console/network capture config; absent ⇒ default-on (#138). */
84
+ readonly capture?: CaptureConfig;
85
+ /**
86
+ * Where the SDK opens **Connect** (ADR-0032) — the platform's connect page, built
87
+ * by the server from its configured dashboard origin and carried on every boot so
88
+ * the SDK does not need to know the Fixback dashboard origin itself. Absent from an
89
+ * older server; the SDK then cannot offer Connect.
90
+ */
91
+ readonly connectUrl?: string;
92
+ /**
93
+ * The signed-in Account when a valid Reporter session was presented, else `null`.
94
+ * Lets the identity chip show the person's name on a page reload, when the SDK holds
95
+ * only the stored token.
96
+ */
97
+ readonly account?: ConnectedAccount | null;
98
+ /**
99
+ * A **refreshed** Reporter session token when the presented one was valid (30-day
100
+ * sliding lifetime; ADR-0032). The SDK swaps its stored token for this on every
101
+ * boot, so an actively-used session never lapses. Absent when no valid session was
102
+ * presented.
103
+ */
104
+ readonly reporterSession?: string;
105
+ }
106
+ /** Join an API base URL with the boot path, tolerating a trailing slash. */
107
+ export declare function bootEndpoint(apiUrl: string): string;
108
+ /** Join an API base URL with the feedback path, tolerating a trailing slash. */
109
+ export declare function feedbackEndpoint(apiUrl: string): string;
110
+ /** Narrow an unknown JSON body to a {@link BootAnswer} before an SDK trusts it. */
111
+ export declare function isBootAnswer(value: unknown): value is BootAnswer;
112
+ /** Drop `undefined` identity fields so a payload carries only what was given. */
113
+ export declare function compactIdentity(identity: IdentityInputs | undefined): IdentityInputs;
114
+ /**
115
+ * Does `url` address one of the SDK's **own** API endpoints? Every request a capture
116
+ * SDK issues lives under two namespaces beneath the API origin: `/api/ingest/*` —
117
+ * boot and feedback — and `/api/connect/*` — the Connect code exchange and sign-out
118
+ * (ADR-0032).
119
+ *
120
+ * The trace's network capture ignores **only** these, never the whole origin. That
121
+ * distinction matters when the instrumented app and the Fixback API share an origin — a
122
+ * self-hosted deployment, or Fixback running its own SDK on its own dashboard — where
123
+ * ignoring the origin wholesale would swallow all of the app's own requests and leave the
124
+ * Network tab empty. When the app and the API sit on different origins (the common case)
125
+ * this matches nothing extra, since the app's origin is never `apiUrl`. `url` is the raw
126
+ * request URL — absolute for every call an SDK makes — so a prefix test over the
127
+ * normalised origin is exact.
128
+ */
129
+ export declare function isFixbackApiRequest(apiUrl: string, url: string): boolean;
130
+ /** Injectable collaborators for {@link requestBoot}. */
131
+ export interface BootDeps {
132
+ /** The `fetch` to call. Defaults to the runtime's global. */
133
+ readonly fetch?: FetchLike;
134
+ /**
135
+ * The origin to declare explicitly, for a runtime that attaches no `Origin`
136
+ * header of its own (React Native). Omit in a browser — the user agent sets it,
137
+ * and a page may not override it.
138
+ */
139
+ readonly origin?: string;
140
+ }
141
+ /**
142
+ * Ask ingest whether a submission would be accepted for this key / origin / Gate.
143
+ * Resolves to the boot answer, or `null` when Fixback could not be reached, the key
144
+ * was refused, or the response was not a boot answer. It never throws: any
145
+ * non-answer is treated by the caller as "stay dormant", so a Fixback outage stays
146
+ * invisible to the host app (ticket #47: "fails quietly").
147
+ */
148
+ export declare function requestBoot(apiUrl: string, request: BootRequest, deps?: BootDeps): Promise<BootAnswer | null>;
package/dist/boot.js ADDED
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ /**
3
+ * The ingest **boot + feedback wire contract** — the one copy every capture SDK
4
+ * speaks (ADR-0028).
5
+ *
6
+ * The SDKs deliberately do not import `@fixback/shared` — that package is private
7
+ * and server-shaped (ticket #47) — so the exact slice they need lives here, in the
8
+ * shared core, rather than being vendored once per SDK. Keep it in lock-step with
9
+ * the server: the request body accepted by `POST /api/ingest/boot`
10
+ * (`apps/api/src/ingest/ingest.controller.ts`) and the `BootAnswer` returned by
11
+ * `evaluateBoot` (`apps/api/src/ingest/reporter-identity.ts`).
12
+ *
13
+ * One runtime difference is carried as an option, not a fork: a browser attaches
14
+ * the `Origin` header itself, while a native HTTP stack attaches none — so React
15
+ * Native passes its configured `origin` and the server reads it to decide
16
+ * `originAllowed` exactly as it does for a browser request (spec 0004 §A).
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.bootEndpoint = bootEndpoint;
20
+ exports.feedbackEndpoint = feedbackEndpoint;
21
+ exports.isBootAnswer = isBootAnswer;
22
+ exports.compactIdentity = compactIdentity;
23
+ exports.isFixbackApiRequest = isFixbackApiRequest;
24
+ exports.requestBoot = requestBoot;
25
+ const http_1 = require("./http");
26
+ /** Join an API base URL with the boot path, tolerating a trailing slash. */
27
+ function bootEndpoint(apiUrl) {
28
+ return `${apiUrl.replace(/\/+$/, "")}/api/ingest/boot`;
29
+ }
30
+ /** Join an API base URL with the feedback path, tolerating a trailing slash. */
31
+ function feedbackEndpoint(apiUrl) {
32
+ return `${apiUrl.replace(/\/+$/, "")}/api/ingest/feedback`;
33
+ }
34
+ /** Narrow an unknown JSON body to a {@link BootAnswer} before an SDK trusts it. */
35
+ function isBootAnswer(value) {
36
+ if (typeof value !== "object" || value === null)
37
+ return false;
38
+ const v = value;
39
+ return (typeof v.originAllowed === "boolean" &&
40
+ typeof v.canSubmit === "boolean" &&
41
+ typeof v.gate === "string" &&
42
+ (v.tier === null || typeof v.tier === "string"));
43
+ }
44
+ /** Drop `undefined` identity fields so a payload carries only what was given. */
45
+ function compactIdentity(identity) {
46
+ if (!identity)
47
+ return {};
48
+ const out = {};
49
+ if (identity.hostIdentity)
50
+ out.hostIdentity = identity.hostIdentity;
51
+ if (identity.reporterSession)
52
+ out.reporterSession = identity.reporterSession;
53
+ if (identity.anonymousId)
54
+ out.anonymousId = identity.anonymousId;
55
+ return out;
56
+ }
57
+ /**
58
+ * Does `url` address one of the SDK's **own** API endpoints? Every request a capture
59
+ * SDK issues lives under two namespaces beneath the API origin: `/api/ingest/*` —
60
+ * boot and feedback — and `/api/connect/*` — the Connect code exchange and sign-out
61
+ * (ADR-0032).
62
+ *
63
+ * The trace's network capture ignores **only** these, never the whole origin. That
64
+ * distinction matters when the instrumented app and the Fixback API share an origin — a
65
+ * self-hosted deployment, or Fixback running its own SDK on its own dashboard — where
66
+ * ignoring the origin wholesale would swallow all of the app's own requests and leave the
67
+ * Network tab empty. When the app and the API sit on different origins (the common case)
68
+ * this matches nothing extra, since the app's origin is never `apiUrl`. `url` is the raw
69
+ * request URL — absolute for every call an SDK makes — so a prefix test over the
70
+ * normalised origin is exact.
71
+ */
72
+ function isFixbackApiRequest(apiUrl, url) {
73
+ const apiBase = apiUrl.replace(/\/+$/, "");
74
+ return (url.startsWith(`${apiBase}/api/ingest/`) ||
75
+ url.startsWith(`${apiBase}/api/connect/`));
76
+ }
77
+ /**
78
+ * Ask ingest whether a submission would be accepted for this key / origin / Gate.
79
+ * Resolves to the boot answer, or `null` when Fixback could not be reached, the key
80
+ * was refused, or the response was not a boot answer. It never throws: any
81
+ * non-answer is treated by the caller as "stay dormant", so a Fixback outage stays
82
+ * invisible to the host app (ticket #47: "fails quietly").
83
+ */
84
+ async function requestBoot(apiUrl, request, deps = {}) {
85
+ const doFetch = deps.fetch ?? (0, http_1.resolveFetch)();
86
+ if (!doFetch)
87
+ return null;
88
+ let response;
89
+ try {
90
+ response = await doFetch(bootEndpoint(apiUrl), {
91
+ method: "POST",
92
+ headers: {
93
+ "content-type": "application/json",
94
+ ...(deps.origin ? { origin: deps.origin } : {}),
95
+ },
96
+ body: JSON.stringify(request),
97
+ });
98
+ }
99
+ catch {
100
+ return null; // network error / Fixback unreachable
101
+ }
102
+ if (!response.ok)
103
+ return null; // 401 unknown key, or any other refusal
104
+ let body;
105
+ try {
106
+ body = await response.json?.();
107
+ }
108
+ catch {
109
+ return null;
110
+ }
111
+ return isBootAnswer(body) ? body : null;
112
+ }
113
+ //# sourceMappingURL=boot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boot.js","sourceRoot":"","sources":["../src/boot.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,133 @@
1
+ /**
2
+ * The trace **breadcrumb wire types** (spec 0003 §C, spec #122) — the shared,
3
+ * runtime-agnostic shapes for one entry of a captured trace stream (ADR-0028).
4
+ *
5
+ * A breadcrumb records recent activity — console, navigation, network metadata,
6
+ * masked user actions, and the failing error — that rides on a report as
7
+ * Evidence. This module carries only the value types; the SDK's ring buffer and
8
+ * DOM/Node instrumentation that produce them live in the consuming SDK.
9
+ * Everything private is kept out **at the source** by that instrumentation:
10
+ * `ui.input` records that an input changed, never its value; network crumbs carry
11
+ * method + URL + status only, never bodies.
12
+ */
13
+ /** Console-style severity a `console` crumb records. */
14
+ export type BreadcrumbLevel = "log" | "info" | "warn" | "error" | "assert" | "debug";
15
+ /** The kind of activity a crumb records. */
16
+ export type BreadcrumbCategory = "console" | "navigation" | "fetch" | "xhr" | "beacon" | "ui.click" | "ui.input" | "error";
17
+ /**
18
+ * Which browser API issued a captured network request (spec #122 §D, ticket #139).
19
+ * Every network crumb's `category` is one of these too, so a stream router and the
20
+ * read model agree on what is a network entry.
21
+ */
22
+ export type NetworkApi = "fetch" | "xhr" | "beacon";
23
+ /**
24
+ * A network request's failure classification (spec #122 §D, ticket #139). `ok` and
25
+ * the HTTP status classes come from a settled response; `network-error` / `timeout`
26
+ * / `aborted` from how a request failed; `opaque-cors` from a cross-origin response
27
+ * whose status is unreadable. The Network tab flags every non-`ok` outcome.
28
+ */
29
+ export type NetworkOutcome = "ok" | "http-4xx" | "http-5xx" | "network-error" | "timeout" | "aborted" | "opaque-cors";
30
+ /**
31
+ * A crumb's structured detail. Deliberately narrow: there is **no** field for a
32
+ * request/response body or an input value, so those can never be recorded.
33
+ */
34
+ export interface BreadcrumbData {
35
+ readonly url?: string;
36
+ readonly method?: string;
37
+ readonly status?: number;
38
+ /** A masked CSS selector for a `ui.*` target — never its text or value. */
39
+ readonly target?: string;
40
+ readonly from?: string;
41
+ readonly to?: string;
42
+ readonly errorType?: string;
43
+ }
44
+ /**
45
+ * The type tag on a structured console argument (spec #122 §C, decision D7). A
46
+ * console call's arguments are preserved **type-tagged** rather than flattened to a
47
+ * string, so an object/array argument can be inspected in the Console tab rather
48
+ * than read as `[object Object]`: `string`/`number`/`bool`/`null` carry the value
49
+ * directly, `json` a depth-/byte-capped JSON-safe clone, and `error` an Error's
50
+ * `{ name, message, stack }`.
51
+ */
52
+ export type ConsoleArgType = "string" | "number" | "bool" | "null" | "json" | "error";
53
+ /**
54
+ * One structured console argument (spec #122 §C): a type tag plus a JSON-safe value.
55
+ * `v` is always serializable — a `json` arg is depth-, breadth-, and string-capped at
56
+ * assembly, and exotic values (bigint, symbol, function, circular refs) are rendered
57
+ * to safe text — so a console crumb can never carry an unserializable or unbounded
58
+ * value onto the wire.
59
+ */
60
+ export interface ConsoleArg {
61
+ readonly t: ConsoleArgType;
62
+ readonly v: unknown;
63
+ }
64
+ /** A `file:line` source location (spec #122 §C) — where a console call was made. */
65
+ export interface SourceLocation {
66
+ readonly file: string;
67
+ readonly line: number;
68
+ }
69
+ /**
70
+ * One entry in a trace stream. Alongside its semantic fields every entry carries a
71
+ * stable {@link id} and a high-res monotonic {@link mono} timestamp — both stamped
72
+ * by the buffer on `add` — so entries from the three independent streams order and
73
+ * cross-link exactly (spec #122 §B, decision D9). `timestamp` stays epoch ms.
74
+ */
75
+ export interface Breadcrumb {
76
+ readonly category: BreadcrumbCategory;
77
+ readonly message?: string;
78
+ readonly level?: BreadcrumbLevel;
79
+ /** Epoch milliseconds when the crumb was recorded. */
80
+ readonly timestamp: number;
81
+ /** A stable id, unique within the buffer — assigned on `add` when not already set. */
82
+ readonly id?: string;
83
+ /** A high-res monotonic timestamp (`performance.now()`) — assigned on `add`. */
84
+ readonly mono?: number;
85
+ readonly data?: BreadcrumbData;
86
+ /**
87
+ * For a `console` crumb (spec #122 §C): the call's arguments preserved as
88
+ * structured, type-tagged values (see {@link ConsoleArg}), so the Console tab can
89
+ * render each argument expandably instead of a flattened string. `message` stays
90
+ * the one-line preview. Absent on non-console crumbs.
91
+ */
92
+ readonly args?: readonly ConsoleArg[];
93
+ /**
94
+ * For a `console` crumb (spec #122 §C): the `file:line` the call was made from,
95
+ * parsed best-effort from the call stack. Captured only for the levels that keep a
96
+ * source — `warn`/`error`/`assert` — and only when the stack yields a usable
97
+ * location; a chatty app's `log`/`info`/`debug` omit it. Absent on non-console crumbs.
98
+ */
99
+ readonly source?: SourceLocation;
100
+ /**
101
+ * For an auto-captured `error` crumb only (spec #122 §F): the ids of the entries
102
+ * immediately preceding the throw — a causal pointer into the same trace, so a
103
+ * machine-filed crash names its lead-up. Absent on every other crumb.
104
+ */
105
+ readonly causedBy?: readonly string[];
106
+ /**
107
+ * For a network crumb (`fetch` / `xhr` / `beacon`) — spec #122 §D, ticket #139.
108
+ * The rich request metadata the Network tab renders, lifted to the top level (the
109
+ * console-enrichment precedent) so the read model shapes each into a
110
+ * `NetworkTraceEntry`. **No field carries a request/response body or an arbitrary
111
+ * header** — `respSize` derives from the `content-length` response header only and
112
+ * `contentType` from `content-type`; nothing else is read. Absent on every other crumb.
113
+ */
114
+ readonly api?: NetworkApi;
115
+ /** The request method (network crumb) — e.g. `GET`, `POST`. */
116
+ readonly method?: string;
117
+ /** The scrubbed request URL (network crumb) — query dropped, path PII redacted. */
118
+ readonly url?: string;
119
+ /** The final HTTP status (network crumb), when one was known; absent on a network error/beacon. */
120
+ readonly status?: number;
121
+ /** The HTTP status text (network crumb), when the response carried one. */
122
+ readonly statusText?: string;
123
+ /** Wall-clock duration of the request in ms (network crumb) — a `performance.now()` delta. */
124
+ readonly durationMs?: number;
125
+ /** Request body size in bytes (network crumb) — only when trivially known (string/Blob/ArrayBuffer), never by reading a stream. */
126
+ readonly reqSize?: number;
127
+ /** Response body size in bytes (network crumb) — from the `content-length` response header only. */
128
+ readonly respSize?: number;
129
+ /** Response content type (network crumb) — the `content-type` header's media type. */
130
+ readonly contentType?: string;
131
+ /** The request's failure classification (network crumb) — see {@link NetworkOutcome}. */
132
+ readonly outcome?: NetworkOutcome;
133
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The **Connect** wire contract (ADR-0032, CONTEXT.md "Connect") — the one copy every
3
+ * capture SDK speaks, kept in the shared core beside the boot contract for the same
4
+ * reason (`@fixback/shared` is server-shaped and private, ticket #47).
5
+ *
6
+ * Connect is the round trip that binds an Account to a site: the SDK opens the
7
+ * platform's connect page ({@link BootAnswer.connectUrl}), the person signs in, the
8
+ * page hands back a **one-time code**, and the SDK exchanges it here for a **Reporter
9
+ * session** — a compact token it stores per origin and forwards on every boot. Keep
10
+ * this in lock-step with the server: `POST /api/connect/exchange` and
11
+ * `POST /api/connect/signout` (`apps/api/src/connect/*`).
12
+ */
13
+ import type { ConnectedAccount, ReporterTier } from "./boot";
14
+ import { type FetchLike } from "./http";
15
+ /**
16
+ * The `postMessage` a connect page in a popup sends back to the opener. `source`
17
+ * pins it to Fixback so an unrelated message on the window is ignored; `code` is the
18
+ * one-time Connect code to exchange. Read with {@link parseConnectMessage}.
19
+ */
20
+ export declare const CONNECT_MESSAGE_SOURCE = "fixback-connect";
21
+ /**
22
+ * The query parameter a Fixback link — and the connect page's **redirect fallback** —
23
+ * carries the one-time code back on, when a popup could not be used (ADR-0032,
24
+ * "arriving with the `fixback` query parameter"). The SDK detects it on boot, exchanges
25
+ * it, and strips it from the URL.
26
+ */
27
+ export declare const CONNECT_RETURN_PARAM = "fixback";
28
+ /** The shape of the popup-to-opener message; `code` is the one-time Connect code. */
29
+ export interface ConnectMessage {
30
+ readonly source: typeof CONNECT_MESSAGE_SOURCE;
31
+ readonly code: string;
32
+ }
33
+ /**
34
+ * The identity the SDK reports through `Fixback.identity()` and renders in the chip:
35
+ * anonymous, or a connected Account with its live tier. The tier is the server's, from
36
+ * the last boot — never self-declared.
37
+ */
38
+ export type ReporterIdentity = {
39
+ readonly status: "anonymous";
40
+ } | {
41
+ readonly status: "connected";
42
+ readonly name: string;
43
+ readonly email: string;
44
+ readonly tier: ReporterTier | null;
45
+ };
46
+ /** The JSON body `POST /api/connect/exchange` accepts. */
47
+ export interface ConnectExchangeRequest {
48
+ /** The Project's publishable key — the exchange must be for the code's Project. */
49
+ readonly key: string;
50
+ /** The one-time Connect code from the popup message or the return query parameter. */
51
+ readonly code: string;
52
+ /**
53
+ * The browser's current anonymous id, if any — so the server can link the existing
54
+ * anonymous Reporter to the Account (one Reporter, prior Feedback attributed).
55
+ */
56
+ readonly anonymousId?: string;
57
+ /** The Platform the Connect happened on; defaults to `browser` server-side. */
58
+ readonly platform?: "browser" | "expo";
59
+ }
60
+ /** The JSON `POST /api/connect/exchange` returns on success. */
61
+ export interface ConnectExchangeResponse {
62
+ /** The Reporter session token to store per origin and forward on every boot. */
63
+ readonly reporterSession: string;
64
+ /** The signed-in Account, for the identity chip. */
65
+ readonly account: ConnectedAccount;
66
+ }
67
+ /** Join an API base URL with the Connect exchange path, tolerating a trailing slash. */
68
+ export declare function connectExchangeEndpoint(apiUrl: string): string;
69
+ /** Join an API base URL with the Connect sign-out path, tolerating a trailing slash. */
70
+ export declare function connectSignOutEndpoint(apiUrl: string): string;
71
+ /**
72
+ * Build the connect-page URL the SDK opens (popup or redirect). `connectUrl` is the
73
+ * base the boot answer carried; `key` identifies the Project and `returnUrl` is where
74
+ * the person is sent back — validated server-side against the Project's allowed
75
+ * origins, so a hostile page cannot redirect the code anywhere else.
76
+ */
77
+ export declare function buildConnectUrl(connectUrl: string, params: {
78
+ readonly key: string;
79
+ readonly returnUrl: string;
80
+ }): string;
81
+ /**
82
+ * The `localStorage` key a browser SDK stores its Reporter session token under,
83
+ * **scoped by publishable key** so two Fixback Projects on one origin keep separate
84
+ * sessions (ADR-0032). The storage binding is per-runtime; the key format is shared.
85
+ */
86
+ export declare function reporterSessionStorageKey(publishableKey: string): string;
87
+ /**
88
+ * Narrow an unknown `message.data` to the Connect code it carries, or `null`. Guards
89
+ * `source` so an unrelated `postMessage` — the web is noisy — is ignored.
90
+ */
91
+ export declare function parseConnectMessage(data: unknown): string | null;
92
+ /** Narrow an unknown JSON body to a {@link ConnectExchangeResponse}. */
93
+ export declare function isConnectExchangeResponse(value: unknown): value is ConnectExchangeResponse;
94
+ /** Injectable collaborators for the Connect fetch helpers. */
95
+ export interface ConnectDeps {
96
+ readonly fetch?: FetchLike;
97
+ }
98
+ /**
99
+ * Exchange a one-time code for a Reporter session. Resolves to the response, or `null`
100
+ * when Fixback was unreachable, the code was refused, or the body was not a valid
101
+ * response — the caller stays anonymous rather than surfacing an error into the host
102
+ * page (ticket #47: "fails quietly").
103
+ */
104
+ export declare function exchangeConnectCode(apiUrl: string, request: ConnectExchangeRequest, deps?: ConnectDeps): Promise<ConnectExchangeResponse | null>;
105
+ /**
106
+ * Revoke a Reporter session server-side ("sign out"). Best-effort: a network failure
107
+ * still lets the SDK clear its stored token locally, and a revoked-anyway token is
108
+ * refused at the next boot regardless.
109
+ */
110
+ export declare function revokeReporterSession(apiUrl: string, token: string, deps?: ConnectDeps): Promise<void>;