@qitqode/qitqore 0.1.0 → 0.2.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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/dist/device-authorization.d.ts +67 -0
  4. package/dist/device-authorization.d.ts.map +1 -0
  5. package/dist/device-authorization.js +122 -0
  6. package/dist/device-authorization.js.map +1 -0
  7. package/dist/errors.d.ts +36 -0
  8. package/dist/errors.d.ts.map +1 -1
  9. package/dist/errors.js +108 -17
  10. package/dist/errors.js.map +1 -1
  11. package/dist/index.d.ts +10 -2
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +9 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/recovery-capabilities.d.ts +26 -0
  16. package/dist/recovery-capabilities.d.ts.map +1 -0
  17. package/dist/recovery-capabilities.js +113 -0
  18. package/dist/recovery-capabilities.js.map +1 -0
  19. package/dist/recovery-directive.d.ts +20 -0
  20. package/dist/recovery-directive.d.ts.map +1 -0
  21. package/dist/recovery-directive.js +116 -0
  22. package/dist/recovery-directive.js.map +1 -0
  23. package/dist/recovery-lease.d.ts +80 -0
  24. package/dist/recovery-lease.d.ts.map +1 -0
  25. package/dist/recovery-lease.js +257 -0
  26. package/dist/recovery-lease.js.map +1 -0
  27. package/dist/recovery-status.d.ts +22 -0
  28. package/dist/recovery-status.d.ts.map +1 -0
  29. package/dist/recovery-status.js +113 -0
  30. package/dist/recovery-status.js.map +1 -0
  31. package/dist/recovery.d.ts +13 -0
  32. package/dist/recovery.d.ts.map +1 -1
  33. package/dist/recovery.js +94 -55
  34. package/dist/recovery.js.map +1 -1
  35. package/dist/redaction.d.ts +64 -0
  36. package/dist/redaction.d.ts.map +1 -0
  37. package/dist/redaction.js +154 -0
  38. package/dist/redaction.js.map +1 -0
  39. package/dist/route-contract.d.ts +36 -0
  40. package/dist/route-contract.d.ts.map +1 -0
  41. package/dist/route-contract.js +89 -0
  42. package/dist/route-contract.js.map +1 -0
  43. package/dist/stream-payload.d.ts +39 -0
  44. package/dist/stream-payload.d.ts.map +1 -0
  45. package/dist/stream-payload.js +86 -0
  46. package/dist/stream-payload.js.map +1 -0
  47. package/dist/transport.d.ts +10 -0
  48. package/dist/transport.d.ts.map +1 -1
  49. package/dist/transport.js +28 -0
  50. package/dist/transport.js.map +1 -1
  51. package/package.json +12 -2
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 QitQode contributors, Spannings LLC
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
@@ -8,6 +8,28 @@ It is not a CLI, agent runtime, authentication store, policy engine, scheduler,
8
8
  or billing/settlement system. Importing the package creates no directories,
9
9
  reads credentials, changes process settings, or makes network requests.
10
10
 
11
+ ## Repository and workspace
12
+
13
+ The dedicated repository is
14
+ [`forgewisetech/qitqore`](https://github.com/forgewisetech/qitqore).
15
+ It is a private, independently buildable mirror of the canonical
16
+ `products/qitqore` directory in the Forgewise monorepo. Make source changes in
17
+ that canonical directory, then use the workspace's per-product export to update
18
+ the repository; do not maintain a second implementation in either consumer.
19
+ The repository does not contain QitQode, Wud, their credentials, or their state.
20
+
21
+ The library is tracked as its own product in Umbrella. Its own `products/qitqore`
22
+ folder, dedicated repository, and Umbrella tracking are the intended setup;
23
+ a separate Replit preview artifact is not required.
24
+
25
+ Repository metadata here applies to future package releases. The already
26
+ published `@qitqode/qitqore@0.1.0` remains immutable and has not been republished.
27
+ Future releases are licensed under the [MIT License](./LICENSE), the same license
28
+ as QitQode. The copyright holder is QitQode contributors, Spannings LLC.
29
+
30
+ The local source candidate is now `0.2.0`; it is not published. The immutable
31
+ `0.1.0` artifact and all consumer pins remain unchanged.
32
+
11
33
  ## Development
12
34
 
13
35
  - Requires Node.js 24 or newer.
@@ -21,6 +43,57 @@ reads credentials, changes process settings, or makes network requests.
21
43
  The package has no runtime dependencies. Its sole public entry point is the
22
44
  compiled ESM export at `.`.
23
45
 
46
+ ## Released consumer adoption
47
+
48
+ The public package is **`@qitqode/qitqore@0.1.0`**. Install it with
49
+ `npm install --save-exact @qitqode/qitqore@0.1.0`. The independently managed
50
+ QitQode CLI, its backend SDK, and Wud pin that exact release in their own
51
+ manifests and native Bun/npm lockfiles; they do not import sibling source.
52
+
53
+ QitQode retains AI SDK/native-tool adaptation, credential recovery, advisory
54
+ budgets and session policy. Session retries carry an explicit operation ID;
55
+ prompt equality does not identify an operation. Qortex session retries require
56
+ an explicit non-acceptance verdict and no observed output or started tool.
57
+ Unknown or interrupted execution stops without replay, preserving partial text,
58
+ reasoning and interrupted tool records. Wud retains credentials,
59
+ durable operations, authorization, reservations, leases and settlement.
60
+ Both use the shared request lifecycle, SSE decoder and bounded recovery.
61
+ Compatibility fallback requires explicit pre-execution validation evidence.
62
+ An unterminated stream preserves available text/accounting evidence but is not
63
+ reported as successful completion.
64
+
65
+ Hosts may pass `deadlineAt` to `runWithRecovery` to apply one absolute deadline
66
+ to every attempt, retry delay and backoff; a request timeout alone is not an
67
+ operation-wide limit. `getRecoveryStatus` and `getRecoveryCapabilities`
68
+ validate separate Qortex execution and route-capability contracts; neither is
69
+ billing evidence. `parseRecoveryDirective` accepts only bounded decision fields.
70
+ QitQore does not provide exactly-once effects: callers still need stable
71
+ operation IDs, receipts and reconciliation for ambiguous external operations.
72
+ Recovery status reports only a retained Qortex route result. It never proves that
73
+ a caller applied a local change, an external effect happened exactly once, or
74
+ acceptance verification succeeded.
75
+
76
+ From the monorepo, run
77
+ `node products/qitqore/scripts/verify-consumers.mjs --production-install --bun /path/to/bun`
78
+ under Node 24, with Bun 1.3.11. This checks exact manifest/lock integrity,
79
+ compiled public imports in all consumers, and a clean Wud production dependency
80
+ installation without either application's source. Temporary fixtures are removed.
81
+ Package-local tests remain deterministic and independent of this network install.
82
+ Wud's injected-transport and accounting regressions run without Mongo initialization:
83
+ `cd products/wud && npx vitest run --config vitest.qitqore.config.ts`.
84
+
85
+ Publication, packed-candidate verification, released-consumer adoption and
86
+ deployment are separate claims. The package was published and its public tarball
87
+ verified; adopting it locally does **not** push either consumer or deploy it.
88
+ Future core changes require a new immutable package version, package tests and
89
+ packing checks, separate publication approval, then exact consumer pins and
90
+ regenerated native lockfiles.
91
+
92
+ Release 0.1.0's registry manifest does not declare a license. QitQode's generated
93
+ notice inventory identifies that exact release as first-party code without
94
+ inventing a third-party license grant. That release is not rewritten. The 0.2.0
95
+ candidate declares `"license": "MIT"` and ships its `LICENSE` file in the package.
96
+
24
97
  ## Host responsibilities
25
98
 
26
99
  Create a client with an explicit base URL and optional `fetch` implementation.
@@ -40,6 +113,62 @@ produced partial output, or started a side effect. Acceptance, partial output,
40
113
  and side effects block replay. Unknown outcomes require affirmative host
41
114
  evidence and authorization; an idempotency header alone is not evidence.
42
115
 
116
+ Hosts that maintain a durable attempt ledger can use `onAttemptStart` and
117
+ `onAttemptOutcome` to record dispatch and settlement evidence. The outcome hook
118
+ is informational and does not authorize replay; hosts must still use
119
+ `authorizeReplay` for every additional attempt. Hook payloads contain operation
120
+ and attempt identities plus bounded execution facts, never request bodies,
121
+ credentials, or response content.
122
+
43
123
  Errors expose bounded, redacted diagnostic fields and request IDs, not raw
44
124
  response bodies or request credentials. Hosts may retain their existing
45
125
  product-specific error mappings around these shared primitives.
126
+
127
+ ## Shared wire contracts (0.2 candidate)
128
+
129
+ The 0.2 candidate adds one implementation of the public Qortex contracts that
130
+ QitQode and Wud previously duplicated. Each area states what QitQore does and
131
+ what stays with the host.
132
+
133
+ - **Recovery lease and control.** `parseRecoveryStatus` accepts Qortex's
134
+ optional `recoveryLeaseUsage` only when it names the status's request, and the
135
+ status's route whenever the status includes one.
136
+ `parseRecoveryControlEnvelope`, `encodeRecoveryControlEnvelope`, and
137
+ `decodeRecoveryControlHeader` validate the header value Qortex reads. The
138
+ decoder accepts only canonical base64url. The host allocates token budgets,
139
+ persists leases, decides which work may run, and settles outcomes.
140
+ - **Routes and headers.** `recoveryRouteID`, `qortexModuleEndpoint`,
141
+ `qortexRequestHeaders`, and `withRecoveryControlHeader` take every module name,
142
+ identity, and key from the caller. Scoped historical paths such as
143
+ `@bijikyu/omniquery` are preserved exactly. QitQore never chooses between the
144
+ canonical and historical module, never generates a request or idempotency
145
+ identity, and never adds credentials.
146
+ - **SSE payloads.** `SseDecoder` and `iterateSse` frame the stream.
147
+ `classifyQortexStreamPayload` then returns a tagged result: `text`, `json`,
148
+ `usage`, `cost`, `control` (the documented free-fallback routing signal),
149
+ `done`, `error`, or `empty`. Plain tokens stay `text`, malformed or non-object
150
+ JSON stays `text`, and unknown objects stay `json`. The host converts OpenAI
151
+ chunks, tool calls, finish reasons, accounting, and terminal success. A stream
152
+ that ends without a terminator remains incomplete.
153
+ - **Error evidence.** `qortexErrorEvidence` reads the top-level, `error`, and
154
+ `details` envelopes once and keeps only allowlisted fields: acceptance,
155
+ partial output, side-effect markers, a validated recovery directive, and
156
+ whether replay evidence was available. Any non-`false` acceptance signal
157
+ blocks replay, including values QitQore does not recognize. A terminal
158
+ `retryable: false` wins over `true`. Use `hasReplayBlockingEvidence` before
159
+ replaying; absence of evidence is never permission to replay.
160
+ - **Secret redaction.** `createSecretRedactor` is pure and opt-in. The host
161
+ passes exact values and key names. It returns redaction, chunk-safe stream
162
+ redaction, and bounded structural redaction. It does not read the environment,
163
+ keep a global registry, or discover credentials. Pass `patterns: false` for
164
+ model output, where credential-shape heuristics could alter ordinary prose.
165
+ - **Forgewise device authorization.** `parseDeviceAuthorizationStart` validates
166
+ the start response and resolves expiry from an explicit `now`.
167
+ `classifyDeviceTokenPoll` classifies one poll as success, pending, slow-down
168
+ (through `pending`), denied, expired, failed, or invalid. It does not poll,
169
+ sleep, open a browser, persist a code, install a credential, refresh a session,
170
+ or read a clock. The host owns all of those.
171
+
172
+ Acceptance, partial-output, side-effect, and directive fields are evidence for
173
+ the host's replay decision. They are not authorization, billing settlement, or
174
+ proof of an exactly-once effect.
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Stateless Forgewise device-authorization protocol (RFC 8628 response shapes).
3
+ * These functions validate protocol data and classify poll outcomes. They never
4
+ * poll, sleep, open a browser, persist a code, install or refresh a credential,
5
+ * or read a clock: the caller passes the current time explicitly.
6
+ */
7
+ export declare const DEVICE_AUTHORIZATION_DEFAULT_EXPIRES_IN_SECONDS = 900;
8
+ export declare const DEVICE_AUTHORIZATION_DEFAULT_INTERVAL_SECONDS = 5;
9
+ export declare const DEVICE_AUTHORIZATION_SLOW_DOWN_STEP_SECONDS = 5;
10
+ /** Upper bound for any server-supplied polling interval, so a hostile value cannot stall a host. */
11
+ export declare const DEVICE_AUTHORIZATION_MAX_INTERVAL_SECONDS = 3600;
12
+ /** Upper bound for any server-supplied code lifetime. */
13
+ export declare const DEVICE_AUTHORIZATION_MAX_EXPIRES_IN_SECONDS = 86400;
14
+ /** Validated device authorization start, with the expiry resolved to epoch milliseconds. */
15
+ export interface DeviceAuthorizationStart {
16
+ deviceCode: string;
17
+ userCode: string;
18
+ verificationUri: string;
19
+ verificationUriComplete?: string;
20
+ expiresAt: number;
21
+ intervalSeconds: number;
22
+ }
23
+ /** Outcome of one token poll. Only bounded, safe fields are carried. */
24
+ export type DeviceTokenPollOutcome = {
25
+ kind: "success";
26
+ accessToken: string;
27
+ refreshToken?: string;
28
+ expiresInSeconds?: number;
29
+ } | {
30
+ kind: "pending";
31
+ intervalSeconds: number;
32
+ } | {
33
+ kind: "denied";
34
+ } | {
35
+ kind: "expired";
36
+ } | {
37
+ kind: "failed";
38
+ code: string;
39
+ } | {
40
+ kind: "invalid";
41
+ };
42
+ /** Thrown when the authorization-start response is missing a required protocol field. */
43
+ export declare class DeviceAuthorizationProtocolError extends Error {
44
+ constructor(message?: string);
45
+ }
46
+ /**
47
+ * Validate the device authorization response. Required fields must be present
48
+ * and well-formed. Optional timing is clamped, not trusted. `now` is the caller's
49
+ * current epoch time in milliseconds.
50
+ */
51
+ export declare function parseDeviceAuthorizationStart(value: unknown, now: number): DeviceAuthorizationStart;
52
+ /** True once the code's lifetime has elapsed. The boundary instant itself counts as expired. */
53
+ export declare function deviceAuthorizationExpired(input: {
54
+ expiresAt: number;
55
+ now: number;
56
+ }): boolean;
57
+ /**
58
+ * Classify one device-token poll. `intervalSeconds` is the host's current
59
+ * interval, which `slow_down` increases by a bounded step. Errors other than the
60
+ * RFC 8628 set are reported only by their safe code, never by description text.
61
+ */
62
+ export declare function classifyDeviceTokenPoll(input: {
63
+ status: number;
64
+ body: unknown;
65
+ intervalSeconds: number;
66
+ }): DeviceTokenPollOutcome;
67
+ //# sourceMappingURL=device-authorization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-authorization.d.ts","sourceRoot":"","sources":["../src/device-authorization.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAO,MAAM,+CAA+C,MAAM,CAAC;AACnE,eAAO,MAAM,6CAA6C,IAAI,CAAC;AAC/D,eAAO,MAAM,2CAA2C,IAAI,CAAC;AAC7D,oGAAoG;AACpG,eAAO,MAAM,yCAAyC,OAAQ,CAAC;AAC/D,yDAAyD;AACzD,eAAO,MAAM,2CAA2C,QAAS,CAAC;AAQlE,4FAA4F;AAC5F,MAAM,WAAW,wBAAwB;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,wEAAwE;AACxE,MAAM,MAAM,sBAAsB,GAC9B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1F;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,eAAe,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAExB,yFAAyF;AACzF,qBAAa,gCAAiC,SAAQ,KAAK;gBAC7C,OAAO,SAAiE;CAIrF;AA6BD;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,wBAAwB,CA8BnG;AAED,gGAAgG;AAChG,wBAAgB,0BAA0B,CAAC,KAAK,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAE7F;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;CACzB,GAAG,sBAAsB,CAiCzB"}
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Stateless Forgewise device-authorization protocol (RFC 8628 response shapes).
3
+ * These functions validate protocol data and classify poll outcomes. They never
4
+ * poll, sleep, open a browser, persist a code, install or refresh a credential,
5
+ * or read a clock: the caller passes the current time explicitly.
6
+ */
7
+ export const DEVICE_AUTHORIZATION_DEFAULT_EXPIRES_IN_SECONDS = 900;
8
+ export const DEVICE_AUTHORIZATION_DEFAULT_INTERVAL_SECONDS = 5;
9
+ export const DEVICE_AUTHORIZATION_SLOW_DOWN_STEP_SECONDS = 5;
10
+ /** Upper bound for any server-supplied polling interval, so a hostile value cannot stall a host. */
11
+ export const DEVICE_AUTHORIZATION_MAX_INTERVAL_SECONDS = 3_600;
12
+ /** Upper bound for any server-supplied code lifetime. */
13
+ export const DEVICE_AUTHORIZATION_MAX_EXPIRES_IN_SECONDS = 86_400;
14
+ const MAX_CODE_LENGTH = 2_048;
15
+ const MAX_USER_CODE_LENGTH = 64;
16
+ const MAX_URI_LENGTH = 2_048;
17
+ const MAX_TOKEN_LENGTH = 16_384;
18
+ const SAFE_ERROR_CODE = /^[a-z_]{1,64}$/;
19
+ /** Thrown when the authorization-start response is missing a required protocol field. */
20
+ export class DeviceAuthorizationProtocolError extends Error {
21
+ constructor(message = "Forgewise returned an invalid device authorization response.") {
22
+ super(message);
23
+ this.name = "DeviceAuthorizationProtocolError";
24
+ }
25
+ }
26
+ function isRecord(value) {
27
+ return value !== null && typeof value === "object" && !Array.isArray(value);
28
+ }
29
+ function boundedString(value, maxLength) {
30
+ return typeof value === "string" && value.length > 0 && value.length <= maxLength ? value : undefined;
31
+ }
32
+ /** A verification URI must be absolute HTTPS, or plain HTTP only for a loopback host. */
33
+ function verificationUri(value) {
34
+ const text = boundedString(value, MAX_URI_LENGTH);
35
+ if (text === undefined)
36
+ return undefined;
37
+ try {
38
+ const url = new URL(text);
39
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
40
+ return url.protocol === "https:" || (url.protocol === "http:" && loopback) ? text : undefined;
41
+ }
42
+ catch {
43
+ return undefined;
44
+ }
45
+ }
46
+ /** Clamp a server-supplied number to [1, max] and fall back to the default when it is absent or invalid. */
47
+ function boundedSeconds(value, fallback, max) {
48
+ if (typeof value !== "number" || !Number.isFinite(value))
49
+ return fallback;
50
+ return Math.min(max, Math.max(1, Math.floor(value)));
51
+ }
52
+ /**
53
+ * Validate the device authorization response. Required fields must be present
54
+ * and well-formed. Optional timing is clamped, not trusted. `now` is the caller's
55
+ * current epoch time in milliseconds.
56
+ */
57
+ export function parseDeviceAuthorizationStart(value, now) {
58
+ if (!isRecord(value))
59
+ throw new DeviceAuthorizationProtocolError();
60
+ const deviceCode = boundedString(value.device_code, MAX_CODE_LENGTH);
61
+ const userCode = boundedString(value.user_code, MAX_USER_CODE_LENGTH);
62
+ const verification = verificationUri(value.verification_uri);
63
+ if (deviceCode === undefined || userCode === undefined || verification === undefined) {
64
+ throw new DeviceAuthorizationProtocolError();
65
+ }
66
+ const complete = value.verification_uri_complete === undefined ? undefined : verificationUri(value.verification_uri_complete);
67
+ if (value.verification_uri_complete !== undefined && complete === undefined) {
68
+ throw new DeviceAuthorizationProtocolError();
69
+ }
70
+ const expiresIn = boundedSeconds(value.expires_in, DEVICE_AUTHORIZATION_DEFAULT_EXPIRES_IN_SECONDS, DEVICE_AUTHORIZATION_MAX_EXPIRES_IN_SECONDS);
71
+ return {
72
+ deviceCode,
73
+ userCode,
74
+ verificationUri: verification,
75
+ ...(complete === undefined ? {} : { verificationUriComplete: complete }),
76
+ expiresAt: now + expiresIn * 1_000,
77
+ intervalSeconds: boundedSeconds(value.interval, DEVICE_AUTHORIZATION_DEFAULT_INTERVAL_SECONDS, DEVICE_AUTHORIZATION_MAX_INTERVAL_SECONDS),
78
+ };
79
+ }
80
+ /** True once the code's lifetime has elapsed. The boundary instant itself counts as expired. */
81
+ export function deviceAuthorizationExpired(input) {
82
+ return input.now >= input.expiresAt;
83
+ }
84
+ /**
85
+ * Classify one device-token poll. `intervalSeconds` is the host's current
86
+ * interval, which `slow_down` increases by a bounded step. Errors other than the
87
+ * RFC 8628 set are reported only by their safe code, never by description text.
88
+ */
89
+ export function classifyDeviceTokenPoll(input) {
90
+ const body = isRecord(input.body) ? input.body : undefined;
91
+ if (input.status >= 200 && input.status < 300) {
92
+ const accessToken = boundedString(body?.access_token, MAX_TOKEN_LENGTH);
93
+ if (accessToken === undefined)
94
+ return { kind: "invalid" };
95
+ const refreshToken = boundedString(body?.refresh_token, MAX_TOKEN_LENGTH);
96
+ const expiresIn = typeof body?.expires_in === "number" && Number.isFinite(body.expires_in) && body.expires_in > 0
97
+ ? body.expires_in
98
+ : undefined;
99
+ return {
100
+ kind: "success",
101
+ accessToken,
102
+ ...(refreshToken === undefined ? {} : { refreshToken }),
103
+ ...(expiresIn === undefined ? {} : { expiresInSeconds: expiresIn }),
104
+ };
105
+ }
106
+ const error = typeof body?.error === "string" && SAFE_ERROR_CODE.test(body.error) ? body.error : undefined;
107
+ if (error === "authorization_pending") {
108
+ return { kind: "pending", intervalSeconds: input.intervalSeconds };
109
+ }
110
+ if (error === "slow_down") {
111
+ return {
112
+ kind: "pending",
113
+ intervalSeconds: Math.min(DEVICE_AUTHORIZATION_MAX_INTERVAL_SECONDS, input.intervalSeconds + DEVICE_AUTHORIZATION_SLOW_DOWN_STEP_SECONDS),
114
+ };
115
+ }
116
+ if (error === "access_denied")
117
+ return { kind: "denied" };
118
+ if (error === "expired_token")
119
+ return { kind: "expired" };
120
+ return { kind: "failed", code: error ?? `http_${input.status}` };
121
+ }
122
+ //# sourceMappingURL=device-authorization.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-authorization.js","sourceRoot":"","sources":["../src/device-authorization.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,CAAC,MAAM,+CAA+C,GAAG,GAAG,CAAC;AACnE,MAAM,CAAC,MAAM,6CAA6C,GAAG,CAAC,CAAC;AAC/D,MAAM,CAAC,MAAM,2CAA2C,GAAG,CAAC,CAAC;AAC7D,oGAAoG;AACpG,MAAM,CAAC,MAAM,yCAAyC,GAAG,KAAK,CAAC;AAC/D,yDAAyD;AACzD,MAAM,CAAC,MAAM,2CAA2C,GAAG,MAAM,CAAC;AAElE,MAAM,eAAe,GAAG,KAAK,CAAC;AAC9B,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAChC,MAAM,cAAc,GAAG,KAAK,CAAC;AAC7B,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAChC,MAAM,eAAe,GAAG,gBAAgB,CAAC;AAqBzC,yFAAyF;AACzF,MAAM,OAAO,gCAAiC,SAAQ,KAAK;IACzD,YAAY,OAAO,GAAG,8DAA8D;QAClF,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,kCAAkC,CAAC;IACjD,CAAC;CACF;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,aAAa,CAAC,KAAc,EAAE,SAAiB;IACtD,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,IAAI,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AACxG,CAAC;AAED,yFAAyF;AACzF,SAAS,eAAe,CAAC,KAAc;IACrC,MAAM,IAAI,GAAG,aAAa,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;IAClD,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACzC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1B,MAAM,QAAQ,GAAG,CAAC,WAAW,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC5E,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IAChG,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,4GAA4G;AAC5G,SAAS,cAAc,CAAC,KAAc,EAAE,QAAgB,EAAE,GAAW;IACnE,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC1E,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACvD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,6BAA6B,CAAC,KAAc,EAAE,GAAW;IACvE,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,MAAM,IAAI,gCAAgC,EAAE,CAAC;IACnE,MAAM,UAAU,GAAG,aAAa,CAAC,KAAK,CAAC,WAAW,EAAE,eAAe,CAAC,CAAC;IACrE,MAAM,QAAQ,GAAG,aAAa,CAAC,KAAK,CAAC,SAAS,EAAE,oBAAoB,CAAC,CAAC;IACtE,MAAM,YAAY,GAAG,eAAe,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC;IAC7D,IAAI,UAAU,KAAK,SAAS,IAAI,QAAQ,KAAK,SAAS,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;QACrF,MAAM,IAAI,gCAAgC,EAAE,CAAC;IAC/C,CAAC;IACD,MAAM,QAAQ,GACZ,KAAK,CAAC,yBAAyB,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,eAAe,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC/G,IAAI,KAAK,CAAC,yBAAyB,KAAK,SAAS,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC5E,MAAM,IAAI,gCAAgC,EAAE,CAAC;IAC/C,CAAC;IACD,MAAM,SAAS,GAAG,cAAc,CAC9B,KAAK,CAAC,UAAU,EAChB,+CAA+C,EAC/C,2CAA2C,CAC5C,CAAC;IACF,OAAO;QACL,UAAU;QACV,QAAQ;QACR,eAAe,EAAE,YAAY;QAC7B,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,uBAAuB,EAAE,QAAQ,EAAE,CAAC;QACxE,SAAS,EAAE,GAAG,GAAG,SAAS,GAAG,KAAK;QAClC,eAAe,EAAE,cAAc,CAC7B,KAAK,CAAC,QAAQ,EACd,6CAA6C,EAC7C,yCAAyC,CAC1C;KACF,CAAC;AACJ,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,0BAA0B,CAAC,KAAyC;IAClF,OAAO,KAAK,CAAC,GAAG,IAAI,KAAK,CAAC,SAAS,CAAC;AACtC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAIvC;IACC,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IAC3D,IAAI,KAAK,CAAC,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;QAC9C,MAAM,WAAW,GAAG,aAAa,CAAC,IAAI,EAAE,YAAY,EAAE,gBAAgB,CAAC,CAAC;QACxE,IAAI,WAAW,KAAK,SAAS;YAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;QAC1D,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,EAAE,aAAa,EAAE,gBAAgB,CAAC,CAAC;QAC1E,MAAM,SAAS,GAAG,OAAO,IAAI,EAAE,UAAU,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,UAAU,GAAG,CAAC;YAC/G,CAAC,CAAC,IAAI,CAAC,UAAU;YACjB,CAAC,CAAC,SAAS,CAAC;QACd,OAAO;YACL,IAAI,EAAE,SAAS;YACf,WAAW;YACX,GAAG,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC;YACvD,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,gBAAgB,EAAE,SAAS,EAAE,CAAC;SACpE,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAG,OAAO,IAAI,EAAE,KAAK,KAAK,QAAQ,IAAI,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IAC3G,IAAI,KAAK,KAAK,uBAAuB,EAAE,CAAC;QACtC,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,eAAe,EAAE,KAAK,CAAC,eAAe,EAAE,CAAC;IACrE,CAAC;IACD,IAAI,KAAK,KAAK,WAAW,EAAE,CAAC;QAC1B,OAAO;YACL,IAAI,EAAE,SAAS;YACf,eAAe,EAAE,IAAI,CAAC,GAAG,CACvB,yCAAyC,EACzC,KAAK,CAAC,eAAe,GAAG,2CAA2C,CACpE;SACF,CAAC;IACJ,CAAC;IACD,IAAI,KAAK,KAAK,eAAe;QAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IACzD,IAAI,KAAK,KAAK,eAAe;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAC1D,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,IAAI,QAAQ,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;AACnE,CAAC"}
package/dist/errors.d.ts CHANGED
@@ -1,4 +1,29 @@
1
+ import { type QortexRecoveryDirective } from "./recovery-directive.js";
1
2
  export type QortexErrorCategory = "credentials" | "authorization" | "validation" | "rate-limit" | "gateway" | "server" | "http" | "protocol" | "transport" | "timeout";
3
+ /** Server-reported acceptance, restricted to the public states. */
4
+ export type QortexAcceptanceState = "unknown" | "not-accepted" | "accepted";
5
+ /**
6
+ * Bounded replay evidence from an error envelope. Only the fields below survive.
7
+ * `accepted` is set whenever the server reported any acceptance other than
8
+ * `not-accepted`, including an unrecognized value, so ambiguity fails closed.
9
+ * Use `hasReplayBlockingEvidence` rather than reading one field by itself.
10
+ */
11
+ export interface QortexErrorEvidence {
12
+ contractVersion: 1;
13
+ /** True when the error body was a JSON object whose fields could be examined. */
14
+ replayEvidenceAvailable: boolean;
15
+ accepted?: true;
16
+ partialOutput?: true;
17
+ sideEffectStarted?: true;
18
+ acceptance?: QortexAcceptanceState;
19
+ recoveryDirective?: QortexRecoveryDirective;
20
+ /**
21
+ * The server's explicit retryability verdict, from its directive or envelope.
22
+ * Absent when the server stated none, in which case `QortexError.retryable`
23
+ * falls back to the HTTP status.
24
+ */
25
+ retryable?: boolean;
26
+ }
2
27
  export interface QortexErrorDetails {
3
28
  category: QortexErrorCategory;
4
29
  message: string;
@@ -6,13 +31,24 @@ export interface QortexErrorDetails {
6
31
  status?: number;
7
32
  requestId?: string;
8
33
  retryable: boolean;
34
+ evidence?: QortexErrorEvidence;
9
35
  }
36
+ /** The first safe request ID named in an error body, read from the documented envelope locations. */
37
+ export declare function qortexErrorRequestId(payload: unknown): string | undefined;
38
+ /**
39
+ * Read only the bounded evidence from an error payload. Raw strings, unknown
40
+ * fields, and credentials are never copied into the result.
41
+ */
42
+ export declare function qortexErrorEvidence(payload: unknown): QortexErrorEvidence;
43
+ /** True when any server evidence forbids replaying the failed attempt. */
44
+ export declare function hasReplayBlockingEvidence(evidence: QortexErrorEvidence | undefined): boolean;
10
45
  export declare class QortexError extends Error {
11
46
  readonly category: QortexErrorCategory;
12
47
  readonly code?: string;
13
48
  readonly status?: number;
14
49
  readonly requestId?: string;
15
50
  readonly retryable: boolean;
51
+ readonly evidence?: QortexErrorEvidence;
16
52
  constructor(details: QortexErrorDetails, options?: {
17
53
  cause?: unknown;
18
54
  });
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,mBAAmB,GAC3B,aAAa,GACb,eAAe,GACf,YAAY,GACZ,YAAY,GACZ,SAAS,GACT,QAAQ,GACR,MAAM,GACN,UAAU,GACV,WAAW,GACX,SAAS,CAAC;AAEd,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACpB;AAmDD,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;gBAG1B,OAAO,EAAE,kBAAkB,EAC3B,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAO;IAWnC,MAAM,IAAI,kBAAkB;CAU7B;AAED,qBAAa,eAAgB,SAAQ,WAAW;gBAClC,OAAO,EAAE,kBAAkB,EAAE,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAO;CAI3E;AAED,qBAAa,kBAAmB,SAAQ,WAAW;gBACrC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAO;CAYjE;AAED,qBAAa,oBAAqB,SAAQ,WAAW;gBACvC,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAO;CAY9C;AAED,qBAAa,mBAAoB,SAAQ,WAAW;gBACtC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM;CAWjE;AAED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,WAAW,GACpB,eAAe,CA4CjB;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAKpD"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAG/F,MAAM,MAAM,mBAAmB,GAC3B,aAAa,GACb,eAAe,GACf,YAAY,GACZ,YAAY,GACZ,SAAS,GACT,QAAQ,GACR,MAAM,GACN,UAAU,GACV,WAAW,GACX,SAAS,CAAC;AAEd,mEAAmE;AACnE,MAAM,MAAM,qBAAqB,GAAG,SAAS,GAAG,cAAc,GAAG,UAAU,CAAC;AAE5E;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,eAAe,EAAE,CAAC,CAAC;IACnB,iFAAiF;IACjF,uBAAuB,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,EAAE,IAAI,CAAC;IAChB,aAAa,CAAC,EAAE,IAAI,CAAC;IACrB,iBAAiB,CAAC,EAAE,IAAI,CAAC;IACzB,UAAU,CAAC,EAAE,qBAAqB,CAAC;IACnC,iBAAiB,CAAC,EAAE,uBAAuB,CAAC;IAC5C;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,mBAAmB,CAAC;CAChC;AAsCD,qGAAqG;AACrG,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAIzE;AA8BD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,mBAAmB,CAczE;AA4BD,0EAA0E;AAC1E,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,mBAAmB,GAAG,SAAS,GAAG,OAAO,CAQ5F;AAQD,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,mBAAmB,CAAC;gBAGtC,OAAO,EAAE,kBAAkB,EAC3B,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAO;IAYnC,MAAM,IAAI,kBAAkB;CAW7B;AAED,qBAAa,eAAgB,SAAQ,WAAW;gBAClC,OAAO,EAAE,kBAAkB,EAAE,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAO;CAI3E;AAED,qBAAa,kBAAmB,SAAQ,WAAW;gBACrC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAO;CAYjE;AAED,qBAAa,oBAAqB,SAAQ,WAAW;gBACvC,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAO;CAY9C;AAED,qBAAa,mBAAoB,SAAQ,WAAW;gBACtC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM;CAWjE;AAED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,WAAW,GACpB,eAAe,CA+CjB;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAKpD"}
package/dist/errors.js CHANGED
@@ -1,10 +1,9 @@
1
+ import { parseRecoveryDirective } from "./recovery-directive.js";
2
+ import { redactSensitiveText } from "./redaction.js";
1
3
  const SAFE_CODE = /^[A-Za-z0-9_.:-]{1,128}$/;
2
4
  const SAFE_REQUEST_ID = /^[A-Za-z0-9_.:-]{1,160}$/;
3
- const AUTH_HEADER = /\b(authorization)\s*[:=]\s*(?:bearer|basic)\s+\S+/gi;
4
- const SECRET_VALUE = /\b(api[-_ ]?key|access[-_ ]?token|refresh[-_ ]?token|token|password|secret)\s*[:=]\s*["']?[^\s,;"'&]+/gi;
5
- const BEARER_TOKEN = /\b(bearer|basic)\s+[A-Za-z0-9._~+/-]+=*/gi;
6
- const URL_SECRET = /([?&](?:api[-_]?key|access[-_]?token|refresh[-_]?token|token|secret)=)[^&\s]+/gi;
7
- const JWT = /\beyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g;
5
+ const ACCEPTANCE_STATES = new Set(["unknown", "not-accepted", "accepted"]);
6
+ const REPLAY_FLAGS = ["accepted", "partialOutput", "sideEffectStarted"];
8
7
  function record(value) {
9
8
  return value !== null && typeof value === "object" && !Array.isArray(value)
10
9
  ? value
@@ -13,16 +12,7 @@ function record(value) {
13
12
  function boundedText(value, maxLength) {
14
13
  if (typeof value !== "string")
15
14
  return undefined;
16
- const safe = value
17
- .replace(AUTH_HEADER, "$1: [redacted]")
18
- .replace(SECRET_VALUE, "$1=[redacted]")
19
- .replace(BEARER_TOKEN, "$1 [redacted]")
20
- .replace(URL_SECRET, "$1[redacted]")
21
- .replace(JWT, "[redacted-token]")
22
- .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ")
23
- .trim()
24
- .slice(0, maxLength);
25
- return safe || undefined;
15
+ return redactSensitiveText(value, maxLength) || undefined;
26
16
  }
27
17
  function safeCode(value) {
28
18
  if (typeof value !== "string")
@@ -42,12 +32,108 @@ function safeRequestId(value) {
42
32
  const candidate = value?.trim();
43
33
  return candidate && SAFE_REQUEST_ID.test(candidate) ? candidate : undefined;
44
34
  }
35
+ /** The first safe request ID named in an error body, read from the documented envelope locations. */
36
+ export function qortexErrorRequestId(payload) {
37
+ return envelopeRecords(payload)
38
+ .map((item) => safeRequestId(typeof item.requestId === "string" ? item.requestId : undefined))
39
+ .find((value) => value !== undefined);
40
+ }
41
+ /**
42
+ * The error envelope may appear at the top level, under `error`, or under
43
+ * `details`. Those are the documented locations, and each is read once here,
44
+ * before the raw payload is discarded.
45
+ */
46
+ function envelopeRecords(payload) {
47
+ const outer = record(payload);
48
+ if (!outer)
49
+ return [];
50
+ return [outer, record(outer.error), record(outer.details)].filter((item) => item !== undefined);
51
+ }
52
+ function replayEvidence(records) {
53
+ const evidence = {};
54
+ for (const flag of REPLAY_FLAGS) {
55
+ // Any value other than an explicit `false` is ambiguous, and ambiguity blocks replay.
56
+ if (records.some((item) => item[flag] != null && item[flag] !== false))
57
+ evidence[flag] = true;
58
+ }
59
+ const reported = records.map((item) => item.acceptance).filter((value) => value != null);
60
+ if (reported.some((value) => value !== "not-accepted"))
61
+ evidence.accepted = true;
62
+ if (reported.length === 0)
63
+ return evidence;
64
+ if (reported.includes("accepted"))
65
+ evidence.acceptance = "accepted";
66
+ else if (reported.includes("unknown"))
67
+ evidence.acceptance = "unknown";
68
+ else if (reported.every((value) => value === "not-accepted"))
69
+ evidence.acceptance = "not-accepted";
70
+ return evidence;
71
+ }
72
+ /**
73
+ * Read only the bounded evidence from an error payload. Raw strings, unknown
74
+ * fields, and credentials are never copied into the result.
75
+ */
76
+ export function qortexErrorEvidence(payload) {
77
+ const records = envelopeRecords(payload);
78
+ if (records.length === 0)
79
+ return { contractVersion: 1, replayEvidenceAvailable: false };
80
+ const directiveSource = records.map((item) => item.recoveryDirective).find((value) => value !== undefined);
81
+ const directive = directiveSource === undefined ? undefined : parseDirectiveSafely(directiveSource);
82
+ const retryable = explicitRetryable(records, directive);
83
+ return {
84
+ contractVersion: 1,
85
+ replayEvidenceAvailable: true,
86
+ ...replayEvidence(records),
87
+ ...(directive === undefined ? {} : { recoveryDirective: directive }),
88
+ ...(retryable === undefined ? {} : { retryable }),
89
+ };
90
+ }
91
+ /**
92
+ * The server's own verdict. An explicit `false` is terminal. Otherwise a valid
93
+ * directive decides, and then an explicit `true` from the envelope.
94
+ */
95
+ function explicitRetryable(records, directive) {
96
+ const verdicts = records
97
+ .map((item) => item.retryable)
98
+ .filter((value) => typeof value === "boolean");
99
+ if (verdicts.includes(false))
100
+ return false;
101
+ if (directive !== undefined)
102
+ return directive.retryable;
103
+ if (verdicts.includes(true))
104
+ return true;
105
+ return undefined;
106
+ }
107
+ function parseDirectiveSafely(value) {
108
+ // A malformed or future directive is omitted rather than reflected to the caller.
109
+ try {
110
+ return parseRecoveryDirective(value);
111
+ }
112
+ catch {
113
+ return undefined;
114
+ }
115
+ }
116
+ /** True when any server evidence forbids replaying the failed attempt. */
117
+ export function hasReplayBlockingEvidence(evidence) {
118
+ return (evidence !== undefined &&
119
+ (evidence.accepted === true ||
120
+ evidence.partialOutput === true ||
121
+ evidence.sideEffectStarted === true ||
122
+ (evidence.acceptance !== undefined && evidence.acceptance !== "not-accepted")));
123
+ }
124
+ /** The server's explicit verdict wins. Otherwise the HTTP status decides. */
125
+ function resolveRetryable(status, evidence) {
126
+ if (evidence.retryable !== undefined)
127
+ return evidence.retryable;
128
+ return status === 408 || status === 425 || status === 429 || status >= 500;
129
+ }
45
130
  export class QortexError extends Error {
46
131
  category;
47
132
  code;
48
133
  status;
49
134
  requestId;
50
135
  retryable;
136
+ evidence;
51
137
  constructor(details, options = {}) {
52
138
  super(details.message, options.cause === undefined ? undefined : { cause: options.cause });
53
139
  this.name = "QortexError";
@@ -56,6 +142,7 @@ export class QortexError extends Error {
56
142
  this.status = details.status;
57
143
  this.requestId = details.requestId;
58
144
  this.retryable = details.retryable;
145
+ this.evidence = details.evidence;
59
146
  }
60
147
  toJSON() {
61
148
  return {
@@ -65,6 +152,7 @@ export class QortexError extends Error {
65
152
  ...(this.status === undefined ? {} : { status: this.status }),
66
153
  ...(this.requestId ? { requestId: this.requestId } : {}),
67
154
  retryable: this.retryable,
155
+ ...(this.evidence ? { evidence: this.evidence } : {}),
68
156
  };
69
157
  }
70
158
  }
@@ -112,6 +200,7 @@ export class QortexProtocolError extends QortexError {
112
200
  export function qortexHttpError(status, payload, headers) {
113
201
  const outer = record(payload);
114
202
  const nested = record(outer?.error);
203
+ const evidence = qortexErrorEvidence(payload);
115
204
  const code = safeCode(outer?.code) ??
116
205
  safeCode(nested?.code) ??
117
206
  (typeof outer?.error === "string" ? safeCode(outer.error) : undefined) ??
@@ -124,7 +213,8 @@ export function qortexHttpError(status, payload, headers) {
124
213
  const requestHeaders = new Headers(headers);
125
214
  const requestId = safeRequestId(requestHeaders.get("x-request-id")) ??
126
215
  safeRequestId(requestHeaders.get("request-id")) ??
127
- safeRequestId(requestHeaders.get("x-qlex-request-id"));
216
+ safeRequestId(requestHeaders.get("x-qlex-request-id")) ??
217
+ qortexErrorRequestId(payload);
128
218
  const envelopeFailure = outer?.success === false;
129
219
  let category;
130
220
  if (envelopeFailure && status < 400)
@@ -153,7 +243,8 @@ export function qortexHttpError(status, payload, headers) {
153
243
  code,
154
244
  status,
155
245
  ...(requestId ? { requestId } : {}),
156
- retryable: status === 408 || status === 425 || status === 429 || status >= 500,
246
+ retryable: resolveRetryable(status, evidence),
247
+ evidence,
157
248
  });
158
249
  }
159
250
  export function isAbortError(error) {