@capxul/sdk 4.2.0 → 4.20.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{signer-C0hZ6Kiy.d.mts → OAuthBearerAuthClient-C-ip-z8M.d.mts} +30 -6
- package/dist/{clock-DIfTX44d.mjs → OAuthBearerAuthClient-DDD0JlaI.mjs} +379 -27
- package/dist/index.d.mts +75 -51
- package/dist/index.mjs +144 -3569
- package/dist/node/index.d.mts +2 -3
- package/dist/node/index.mjs +1 -2
- package/dist/{create-capxul-client-B6PUjdld.mjs → production-BmsFhDUr.mjs} +8998 -3076
- package/dist/{observation-vegBPfXj.d.mts → production-DvGDNIwu.d.mts} +298 -84
- package/dist/testing/index.d.mts +82 -2
- package/dist/testing/index.mjs +101 -4
- package/package.json +7 -7
- package/dist/OAuthBearerAuthClient-ByPzJZr8.mjs +0 -211
- package/dist/OAuthBearerAuthClient-D-DLYlKj.d.mts +0 -19
package/dist/testing/index.d.mts
CHANGED
|
@@ -1,5 +1,85 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { h as CapxulResult } from "../OAuthBearerAuthClient-C-ip-z8M.mjs";
|
|
2
|
+
import { er as TelemetryEvent, f as CapxulClient, i as ObservationAdapter, n as ProductionSignerControls, nr as TelemetryIdentifyInput, pr as IdentityTransition, t as CapxulClientInput, tr as TelemetryGroupInput } from "../production-DvGDNIwu.mjs";
|
|
2
3
|
import { Effect, Layer } from "effect";
|
|
4
|
+
//#region src/testing/production-client.d.ts
|
|
5
|
+
/** Create the one production client with per-client controls at its signing boundaries. */
|
|
6
|
+
declare function createCapxulProductionTestClient(input: CapxulClientInput, controls: ProductionSignerControls): Promise<CapxulResult<CapxulClient>>;
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/testing/signer-faults.d.ts
|
|
9
|
+
/**
|
|
10
|
+
* Which provider refusal to inject. The codes are the ones the SDK's own
|
|
11
|
+
* classifiers decode (`src/signer.ts`), so a fault reaches the public error
|
|
12
|
+
* contract through the real decode path rather than a shortcut.
|
|
13
|
+
*
|
|
14
|
+
* - `missing-signer` — Openfort's typed not-ready code. `openfortSignerNotReadyCode`
|
|
15
|
+
* decodes it, so the production recovery wrapper runs its single retry (A02).
|
|
16
|
+
* - `user-rejected` — the EIP-1193 4001 result. `signerFailure` decodes it to
|
|
17
|
+
* `SIGNER_REJECTED`, an expected refusal (A08).
|
|
18
|
+
* - `unexpected` — no provider code at all, so it stays an unexpected
|
|
19
|
+
* `PROVIDER_ERROR` (A09).
|
|
20
|
+
*/
|
|
21
|
+
type SignerFaultKind = "missing-signer" | "user-rejected" | "unexpected";
|
|
22
|
+
/** The value a controlled fault throws. Named so a test can assert its origin. */
|
|
23
|
+
declare class ControlledSignerFault extends Error {
|
|
24
|
+
readonly kind: SignerFaultKind;
|
|
25
|
+
/** Provider code the SDK classifiers read; absent for `unexpected`. */
|
|
26
|
+
readonly code: string | number | undefined;
|
|
27
|
+
constructor(kind: SignerFaultKind);
|
|
28
|
+
}
|
|
29
|
+
/** Build one fault value without the harness, for a caller that writes its own control. */
|
|
30
|
+
declare function signerFaultCause(kind: SignerFaultKind): ControlledSignerFault;
|
|
31
|
+
interface SignerFaultPlan {
|
|
32
|
+
/**
|
|
33
|
+
* Fault the next embedded signature ONCE, beneath the production recovery
|
|
34
|
+
* wrapper, then delegate every later call to the real signer. The address
|
|
35
|
+
* read and the readiness cycle still run first, so the fault lands at the
|
|
36
|
+
* actual final signing boundary after the prepared-command equality checks.
|
|
37
|
+
*/
|
|
38
|
+
readonly signOnce?: SignerFaultKind;
|
|
39
|
+
/**
|
|
40
|
+
* Call the production signer's own `resetSession()` ONCE, immediately before
|
|
41
|
+
* the next signature and after the command is prepared. The delegate then
|
|
42
|
+
* re-runs the real readiness cycle, so the configure request under test is
|
|
43
|
+
* the first one the browser observes after the `reset-session` record (A03).
|
|
44
|
+
*
|
|
45
|
+
* Throws when the composed signer owns no session — a silently skipped reset
|
|
46
|
+
* would leave the run looking green while proving nothing.
|
|
47
|
+
*/
|
|
48
|
+
readonly resetSessionBeforeNextSign?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** What the seam did. JSON-serializable, so a browser test can read it back. */
|
|
51
|
+
interface SignerSeamRecord {
|
|
52
|
+
readonly boundary: "reset-session" | "embedded-sign";
|
|
53
|
+
/** 1-based count of calls at that boundary. */
|
|
54
|
+
readonly attempt: number;
|
|
55
|
+
readonly hash?: string;
|
|
56
|
+
/** Present only on the call that actually threw. */
|
|
57
|
+
readonly fault?: SignerFaultKind;
|
|
58
|
+
readonly atMs: number;
|
|
59
|
+
}
|
|
60
|
+
interface ArmedSignerSeam {
|
|
61
|
+
/** Hand these to `createCapxulProductionTestClient`. */
|
|
62
|
+
readonly controls: ProductionSignerControls;
|
|
63
|
+
/** Oldest first. Empty until the client reaches a signing boundary. */
|
|
64
|
+
readonly records: () => readonly SignerSeamRecord[];
|
|
65
|
+
/** True once every armed one-shot has fired. */
|
|
66
|
+
readonly spent: () => boolean;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Arm one-shot faults on the production embedded signer.
|
|
70
|
+
*
|
|
71
|
+
* ```ts
|
|
72
|
+
* const seam = armSignerFaults({ signOnce: "user-rejected" });
|
|
73
|
+
* const client = await createCapxulProductionTestClient(input, seam.controls);
|
|
74
|
+
* const result = await client.org(orgId).permissions.create(command);
|
|
75
|
+
* // result.error.code === "SIGNER_REJECTED"; nothing was submitted.
|
|
76
|
+
* // seam.records() names the boundary that refused.
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* An empty plan installs no controls, which composes the ordinary client.
|
|
80
|
+
*/
|
|
81
|
+
declare function armSignerFaults(plan?: SignerFaultPlan): ArmedSignerSeam;
|
|
82
|
+
//#endregion
|
|
3
83
|
//#region src/testing/telemetry/RecordingTelemetryAdapter.d.ts
|
|
4
84
|
type RecordingTelemetryOperation = {
|
|
5
85
|
readonly type: "emit";
|
|
@@ -51,4 +131,4 @@ interface CapxulTestObservation {
|
|
|
51
131
|
*/
|
|
52
132
|
declare function createCapxulTestClient(options?: CreateCapxulTestClientOptions): CapxulTestClient;
|
|
53
133
|
//#endregion
|
|
54
|
-
export { CapxulTestClient, CapxulTestClock, CapxulTestObservation, CreateCapxulTestClientOptions, SeedTestIdentityInput, createCapxulTestClient };
|
|
134
|
+
export { type ArmedSignerSeam, CapxulTestClient, CapxulTestClock, CapxulTestObservation, ControlledSignerFault, CreateCapxulTestClientOptions, type ProductionSignerControls, SeedTestIdentityInput, type SignerFaultKind, type SignerFaultPlan, type SignerSeamRecord, armSignerFaults, createCapxulProductionTestClient, createCapxulTestClient, signerFaultCause };
|
package/dist/testing/index.mjs
CHANGED
|
@@ -1,8 +1,105 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { Effect, Result, Semaphore } from "effect";
|
|
1
|
+
import { D as fromWei, P as redactTelemetryEvent, a as smartAccountErrorFromCapxul, c as identityErrorFromCapxul, i as assembleCapxulClient, l as convexCallErrorFromCapxul, n as createCapxulClientWithSignerControls, o as accountReadErrorFromCapxul, s as wireChainId, u as bootstrapErrorFromCapxul, v as toWei } from "../production-BmsFhDUr.mjs";
|
|
2
|
+
import { G as toHandle, H as toEmail, I as toAuthUserId, K as toJwtToken, M as toAddress, N as toAllowedOrigin, P as toAppId, Q as toPublishableKey, R as toChainId, U as toEpochMs, V as toDurationMs, W as toEpochSeconds, ct as CapxulError, d as authClientPortFromPromiseAdapter, et as toSessionToken, it as validateHandle, j as toAccountId, n as readClockNow, q as toKycTier, r as InMemoryAuthCacheAdapter, ut as Errors, y as deriveCapxulSafeAddress, z as toCountryCode } from "../OAuthBearerAuthClient-DDD0JlaI.mjs";
|
|
4
3
|
import { keccak256 } from "viem";
|
|
4
|
+
import { Effect, Result, Semaphore } from "effect";
|
|
5
5
|
import { getFunctionName } from "convex/server";
|
|
6
|
+
//#region src/testing/production-client.ts
|
|
7
|
+
/** Create the one production client with per-client controls at its signing boundaries. */
|
|
8
|
+
function createCapxulProductionTestClient(input, controls) {
|
|
9
|
+
return createCapxulClientWithSignerControls(input, controls);
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/testing/signer-faults.ts
|
|
13
|
+
const FAULT_CODES = {
|
|
14
|
+
"missing-signer": "MISSING_SIGNER",
|
|
15
|
+
"user-rejected": 4001,
|
|
16
|
+
unexpected: void 0
|
|
17
|
+
};
|
|
18
|
+
/** The value a controlled fault throws. Named so a test can assert its origin. */
|
|
19
|
+
var ControlledSignerFault = class extends Error {
|
|
20
|
+
kind;
|
|
21
|
+
/** Provider code the SDK classifiers read; absent for `unexpected`. */
|
|
22
|
+
code;
|
|
23
|
+
constructor(kind) {
|
|
24
|
+
super(`controlled signer fault (${kind})`);
|
|
25
|
+
this.name = "ControlledSignerFault";
|
|
26
|
+
this.kind = kind;
|
|
27
|
+
this.code = FAULT_CODES[kind];
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/** Build one fault value without the harness, for a caller that writes its own control. */
|
|
31
|
+
function signerFaultCause(kind) {
|
|
32
|
+
return new ControlledSignerFault(kind);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Arm one-shot faults on the production embedded signer.
|
|
36
|
+
*
|
|
37
|
+
* ```ts
|
|
38
|
+
* const seam = armSignerFaults({ signOnce: "user-rejected" });
|
|
39
|
+
* const client = await createCapxulProductionTestClient(input, seam.controls);
|
|
40
|
+
* const result = await client.org(orgId).permissions.create(command);
|
|
41
|
+
* // result.error.code === "SIGNER_REJECTED"; nothing was submitted.
|
|
42
|
+
* // seam.records() names the boundary that refused.
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* An empty plan installs no controls, which composes the ordinary client.
|
|
46
|
+
*/
|
|
47
|
+
function armSignerFaults(plan = {}) {
|
|
48
|
+
const records = [];
|
|
49
|
+
let pendingFault = plan.signOnce;
|
|
50
|
+
let pendingReset = plan.resetSessionBeforeNextSign === true;
|
|
51
|
+
let resetCalls = 0;
|
|
52
|
+
let embeddedCalls = 0;
|
|
53
|
+
function record(entry) {
|
|
54
|
+
records.push({
|
|
55
|
+
...entry,
|
|
56
|
+
atMs: Date.now()
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const resetThenSign = async (hash, signer) => {
|
|
60
|
+
if (pendingReset) {
|
|
61
|
+
pendingReset = false;
|
|
62
|
+
resetCalls += 1;
|
|
63
|
+
if (signer.resetSession === void 0) throw new Error("armSignerFaults: resetSessionBeforeNextSign needs the embedded browser signer; this client composed a signer with no session");
|
|
64
|
+
signer.resetSession();
|
|
65
|
+
record({
|
|
66
|
+
boundary: "reset-session",
|
|
67
|
+
attempt: resetCalls,
|
|
68
|
+
hash
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return signer.signUserOpHash(hash);
|
|
72
|
+
};
|
|
73
|
+
const faultThenDelegate = async (hash, delegate) => {
|
|
74
|
+
embeddedCalls += 1;
|
|
75
|
+
const fault = pendingFault;
|
|
76
|
+
if (fault === void 0) {
|
|
77
|
+
record({
|
|
78
|
+
boundary: "embedded-sign",
|
|
79
|
+
attempt: embeddedCalls,
|
|
80
|
+
hash
|
|
81
|
+
});
|
|
82
|
+
return delegate(hash);
|
|
83
|
+
}
|
|
84
|
+
pendingFault = void 0;
|
|
85
|
+
record({
|
|
86
|
+
boundary: "embedded-sign",
|
|
87
|
+
attempt: embeddedCalls,
|
|
88
|
+
hash,
|
|
89
|
+
fault
|
|
90
|
+
});
|
|
91
|
+
throw signerFaultCause(fault);
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
controls: {
|
|
95
|
+
...plan.resetSessionBeforeNextSign === true ? { signUserOpHash: resetThenSign } : {},
|
|
96
|
+
...plan.signOnce === void 0 ? {} : { signEmbeddedUserOpHash: faultThenDelegate }
|
|
97
|
+
},
|
|
98
|
+
records: () => records.slice(),
|
|
99
|
+
spent: () => pendingFault === void 0 && !pendingReset
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
6
103
|
//#region src/testing/account/InMemoryAccountReadAdapter.ts
|
|
7
104
|
var InMemoryAccountReadAdapter = class {
|
|
8
105
|
#deps;
|
|
@@ -909,4 +1006,4 @@ function createCapxulTestClient(options = {}) {
|
|
|
909
1006
|
};
|
|
910
1007
|
}
|
|
911
1008
|
//#endregion
|
|
912
|
-
export { createCapxulTestClient };
|
|
1009
|
+
export { ControlledSignerFault, armSignerFaults, createCapxulProductionTestClient, createCapxulTestClient, signerFaultCause };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capxul/sdk",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.20.0-beta.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/Xelmar-tech/infrastructure.git",
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@effect/platform-node": "4.0.0-
|
|
33
|
+
"@effect/platform-node": "4.0.0-rc.112",
|
|
34
34
|
"@openfort/openfort-js": "^1.3.6",
|
|
35
35
|
"convex": "^1.39.1",
|
|
36
|
-
"effect": "4.0.0-
|
|
36
|
+
"effect": "4.0.0-rc.112",
|
|
37
37
|
"viem": "^2.53.1"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@effect/vitest": "4.0.0-
|
|
40
|
+
"@effect/vitest": "4.0.0-rc.112",
|
|
41
41
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
42
42
|
"@vitest/coverage-v8": "4.1.11",
|
|
43
43
|
"fast-check": "^3.23.2",
|
|
@@ -48,11 +48,11 @@
|
|
|
48
48
|
"vite-plus": "0.3.0",
|
|
49
49
|
"vitest": "4.1.11",
|
|
50
50
|
"@capxul/config": "0.3.0",
|
|
51
|
-
"@capxul/errors": "0.3.0",
|
|
52
51
|
"@capxul/typescript-config": "0.0.0",
|
|
53
52
|
"@capxul/types": "0.3.0",
|
|
54
|
-
"@capxul/
|
|
55
|
-
"@capxul/
|
|
53
|
+
"@capxul/errors": "0.3.0",
|
|
54
|
+
"@capxul/wire": "0.8.0-beta.0",
|
|
55
|
+
"@capxul/observability": "4.20.0-beta.1"
|
|
56
56
|
},
|
|
57
57
|
"_permissionlessPinReason": "permissionless.toSafeSmartAccount is pinned to 0.3.4 for live Safe deployment E2E. Counterfactual address fixtures captured 2026-05-17 in packages/backend/convex/_shared/__tests__/counterfactual.test.ts and packages/config/tests/safe.test.ts must be re-verified before upgrading.",
|
|
58
58
|
"scripts": {
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { A as toAuthUserId, B as toJwtToken, I as toEmail, J as toSessionToken, L as toEpochMs, R as toEpochSeconds, c as ClockError, l as ClockPortTag, t as readClockNow, tt as Errors, u as authClientPortFromPromiseAdapter } from "./clock-DIfTX44d.mjs";
|
|
2
|
-
import { Effect, Layer } from "effect";
|
|
3
|
-
//#region src/adapters/clock/SystemClockAdapter.ts
|
|
4
|
-
var SystemClockAdapter = class {
|
|
5
|
-
now = Effect.try({
|
|
6
|
-
try: () => toEpochMs(Date.now()),
|
|
7
|
-
catch: (cause) => new ClockError({
|
|
8
|
-
operation: "now",
|
|
9
|
-
cause
|
|
10
|
-
})
|
|
11
|
-
});
|
|
12
|
-
sleep(duration) {
|
|
13
|
-
return Effect.callback((resume) => {
|
|
14
|
-
const timeout = setTimeout(() => resume(Effect.void), duration);
|
|
15
|
-
return Effect.sync(() => clearTimeout(timeout));
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
function SystemClockLayer() {
|
|
20
|
-
return Layer.succeed(ClockPortTag, new SystemClockAdapter());
|
|
21
|
-
}
|
|
22
|
-
//#endregion
|
|
23
|
-
//#region src/adapters/auth-client/OAuthBearerAuthClient.ts
|
|
24
|
-
const PROVIDER = "agent-exchange";
|
|
25
|
-
/** Re-exchange this far ahead of expiry, matching the JWT cache eviction margin. */
|
|
26
|
-
const REFRESH_MARGIN_MS = 3e4;
|
|
27
|
-
/**
|
|
28
|
-
* `AuthSession.token` is a Better Auth session handle. This flow has none, and
|
|
29
|
-
* the access token must never be copied into it, so the slot holds a placeholder.
|
|
30
|
-
*/
|
|
31
|
-
const OAUTH_SESSION_TOKEN = toSessionToken("oauth-bearer");
|
|
32
|
-
var OAuthBearerAuthClient = class {
|
|
33
|
-
#exchangeUrl;
|
|
34
|
-
#exchangeSecret;
|
|
35
|
-
#accessToken;
|
|
36
|
-
#fetchImpl;
|
|
37
|
-
#clock;
|
|
38
|
-
#cached = null;
|
|
39
|
-
constructor(input) {
|
|
40
|
-
this.#exchangeUrl = input.exchangeUrl;
|
|
41
|
-
this.#exchangeSecret = input.exchangeSecret;
|
|
42
|
-
this.#accessToken = input.accessToken;
|
|
43
|
-
this.#fetchImpl = input.fetch ?? fetch;
|
|
44
|
-
this.#clock = input.clock ?? new SystemClockAdapter();
|
|
45
|
-
}
|
|
46
|
-
async canSendOtp() {
|
|
47
|
-
return refuse("canSendOtp");
|
|
48
|
-
}
|
|
49
|
-
async sendOtp() {
|
|
50
|
-
return refuse("sendOtp");
|
|
51
|
-
}
|
|
52
|
-
async verifyOtp() {
|
|
53
|
-
return refuse("verifyOtp");
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* The authorization server owns the token's lifetime, so revoking it there is
|
|
57
|
-
* the sign-out. Reporting success here would claim a token was dropped.
|
|
58
|
-
*/
|
|
59
|
-
async signOut() {
|
|
60
|
-
return refuse("signOut");
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* The exchanged identity. Unlike the Better Auth adapters this never answers
|
|
64
|
-
* `null`: the consumer holds a usable access token or a refused one, and a
|
|
65
|
-
* refused one is a NOT_AUTHENTICATED failure, not the absence of a session.
|
|
66
|
-
*/
|
|
67
|
-
async getSession(options) {
|
|
68
|
-
const exchange = await this.#exchange("getSession", options);
|
|
69
|
-
if (!exchange.ok) return exchange;
|
|
70
|
-
return {
|
|
71
|
-
ok: true,
|
|
72
|
-
value: {
|
|
73
|
-
authUserId: toAuthUserId(exchange.value.authUserId),
|
|
74
|
-
email: toEmail(exchange.value.email),
|
|
75
|
-
token: OAUTH_SESSION_TOKEN,
|
|
76
|
-
expiresAt: toEpochMs(exchange.value.expiresAt)
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
async getConvexJwt(options) {
|
|
81
|
-
const exchange = await this.#exchange("getConvexJwt", options);
|
|
82
|
-
if (!exchange.ok) return exchange;
|
|
83
|
-
return {
|
|
84
|
-
ok: true,
|
|
85
|
-
value: {
|
|
86
|
-
token: toJwtToken(exchange.value.token),
|
|
87
|
-
expEpochSeconds: toEpochSeconds(Math.floor(exchange.value.expiresAt / 1e3))
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* The cached exchange while it is fresh, otherwise a new one. A failed
|
|
93
|
-
* exchange never replaces the cache, so a stale token is never served.
|
|
94
|
-
*
|
|
95
|
-
* lazy: no in-flight de-duplication — two concurrent reads past the margin
|
|
96
|
-
* each mint a JWT. Minting is idempotent, so the cost is one extra request.
|
|
97
|
-
*/
|
|
98
|
-
async #exchange(operation, options) {
|
|
99
|
-
if (options?.signal?.aborted) return {
|
|
100
|
-
ok: false,
|
|
101
|
-
error: Errors.cancelled({ operation })
|
|
102
|
-
};
|
|
103
|
-
const cached = this.#cached;
|
|
104
|
-
if (cached !== null && options?.forceRefresh !== true) {
|
|
105
|
-
const now = await readClockNow(this.#clock);
|
|
106
|
-
if (!now.ok) return now;
|
|
107
|
-
if (now.value < cached.expiresAt - REFRESH_MARGIN_MS) return {
|
|
108
|
-
ok: true,
|
|
109
|
-
value: cached
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
return this.#postExchange(operation, options?.signal);
|
|
113
|
-
}
|
|
114
|
-
async #postExchange(operation, signal) {
|
|
115
|
-
let res;
|
|
116
|
-
try {
|
|
117
|
-
res = await this.#fetchImpl(this.#exchangeUrl, {
|
|
118
|
-
method: "POST",
|
|
119
|
-
headers: {
|
|
120
|
-
"content-type": "application/json",
|
|
121
|
-
authorization: `Bearer ${this.#exchangeSecret}`
|
|
122
|
-
},
|
|
123
|
-
body: JSON.stringify({ accessToken: this.#accessToken }),
|
|
124
|
-
...signal === void 0 ? {} : { signal }
|
|
125
|
-
});
|
|
126
|
-
} catch (cause) {
|
|
127
|
-
return {
|
|
128
|
-
ok: false,
|
|
129
|
-
error: signal?.aborted === true || isAbortError(cause) ? Errors.cancelled({ operation }) : Errors.networkError(operation, cause)
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
if (signal?.aborted === true) return {
|
|
133
|
-
ok: false,
|
|
134
|
-
error: Errors.cancelled({ operation })
|
|
135
|
-
};
|
|
136
|
-
const body = await res.json().catch(() => null);
|
|
137
|
-
if (!res.ok) return {
|
|
138
|
-
ok: false,
|
|
139
|
-
error: exchangeRefused(operation, res.status, readErrorCode(body))
|
|
140
|
-
};
|
|
141
|
-
const exchange = readExchange(body);
|
|
142
|
-
if (exchange === null) return {
|
|
143
|
-
ok: false,
|
|
144
|
-
error: Errors.providerError(PROVIDER, operation, /* @__PURE__ */ new Error("unexpected body"), { httpStatus: res.status })
|
|
145
|
-
};
|
|
146
|
-
this.#cached = exchange;
|
|
147
|
-
return {
|
|
148
|
-
ok: true,
|
|
149
|
-
value: exchange
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
/**
|
|
154
|
-
* `AuthClientPort` over the backend agent-token exchange. Build one per OAuth
|
|
155
|
-
* access token and hand it to `createCapxulClient({ authClient })`.
|
|
156
|
-
*/
|
|
157
|
-
function oauthBearerAuthClient(input) {
|
|
158
|
-
return authClientPortFromPromiseAdapter(new OAuthBearerAuthClient(input));
|
|
159
|
-
}
|
|
160
|
-
/** The OTP verbs belong to the email flow; `validStates` names it for the caller. */
|
|
161
|
-
function refuse(method) {
|
|
162
|
-
return {
|
|
163
|
-
ok: false,
|
|
164
|
-
error: Errors.wrongState({
|
|
165
|
-
method,
|
|
166
|
-
currentState: "oauth-bearer",
|
|
167
|
-
validStates: ["email-otp"]
|
|
168
|
-
})
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* The error catalog has no FORBIDDEN code, so 403 (`insufficient_scope`) and
|
|
173
|
-
* 503 (`exchange_unconfigured`) both land on PROVIDER_ERROR. `httpStatus` and
|
|
174
|
-
* `details.reason` carry which one the backend reported. Only 401 becomes
|
|
175
|
-
* NOT_AUTHENTICATED, because only 401 is answered by a new access token.
|
|
176
|
-
*/
|
|
177
|
-
function exchangeRefused(operation, status, reason) {
|
|
178
|
-
if (status === 401) return Errors.notAuthenticated("The exchange refused this OAuth access token");
|
|
179
|
-
return Errors.providerError(PROVIDER, operation, /* @__PURE__ */ new Error(`HTTP ${status}`), {
|
|
180
|
-
httpStatus: status,
|
|
181
|
-
...reason === void 0 ? {} : { details: { reason } }
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
/** An abort raised by something other than the caller's own signal. */
|
|
185
|
-
function isAbortError(cause) {
|
|
186
|
-
return readField(cause, "name") === "AbortError";
|
|
187
|
-
}
|
|
188
|
-
function readErrorCode(body) {
|
|
189
|
-
const error = readField(body, "error");
|
|
190
|
-
return typeof error === "string" ? error : void 0;
|
|
191
|
-
}
|
|
192
|
-
function readField(value, key) {
|
|
193
|
-
if (typeof value !== "object" || value === null) return void 0;
|
|
194
|
-
return value[key];
|
|
195
|
-
}
|
|
196
|
-
function readExchange(body) {
|
|
197
|
-
if (typeof body !== "object" || body === null) return null;
|
|
198
|
-
const { token, expiresAt, authUserId, email } = body;
|
|
199
|
-
if (typeof token !== "string" || token.length === 0) return null;
|
|
200
|
-
if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) return null;
|
|
201
|
-
if (typeof authUserId !== "string" || authUserId.length === 0) return null;
|
|
202
|
-
if (typeof email !== "string" || email.length === 0) return null;
|
|
203
|
-
return {
|
|
204
|
-
token,
|
|
205
|
-
expiresAt,
|
|
206
|
-
authUserId,
|
|
207
|
-
email
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
//#endregion
|
|
211
|
-
export { SystemClockLayer as n, oauthBearerAuthClient as t };
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { _ as ClockPort, v as AuthClientPort } from "./signer-C0hZ6Kiy.mjs";
|
|
2
|
-
//#region src/adapters/auth-client/OAuthBearerAuthClient.d.ts
|
|
3
|
-
interface OAuthBearerAuthClientInput {
|
|
4
|
-
/** The exchange endpoint: `{authBaseUrl}/api/auth/agent/exchange`. */
|
|
5
|
-
readonly exchangeUrl: string;
|
|
6
|
-
/** The shared secret the exchange endpoint checks. The caller holds it. */
|
|
7
|
-
readonly exchangeSecret: string;
|
|
8
|
-
/** The agent's OAuth access token. */
|
|
9
|
-
readonly accessToken: string;
|
|
10
|
-
readonly fetch?: typeof fetch;
|
|
11
|
-
readonly clock?: ClockPort;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* `AuthClientPort` over the backend agent-token exchange. Build one per OAuth
|
|
15
|
-
* access token and hand it to `createCapxulClient({ authClient })`.
|
|
16
|
-
*/
|
|
17
|
-
declare function oauthBearerAuthClient(input: OAuthBearerAuthClientInput): AuthClientPort;
|
|
18
|
-
//#endregion
|
|
19
|
-
export { oauthBearerAuthClient as n, OAuthBearerAuthClientInput as t };
|