@capxul/sdk 0.2.0-alpha.4 → 0.2.0-alpha.5
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/README.md +4 -308
- package/dist/InMemoryAuthCacheAdapter-BK-B_ERB.mjs +113 -0
- package/dist/InMemoryAuthCacheAdapter-BK-B_ERB.mjs.map +1 -0
- package/dist/index.d.mts +1263 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +4740 -0
- package/dist/index.mjs.map +1 -0
- package/dist/node/index.d.mts +55 -0
- package/dist/node/index.d.mts.map +1 -0
- package/dist/node/index.mjs +159 -0
- package/dist/node/index.mjs.map +1 -0
- package/dist/ports/safe-deployment.d.mts +2 -0
- package/dist/ports/safe-deployment.mjs +38 -0
- package/dist/ports/safe-deployment.mjs.map +1 -0
- package/dist/safe-deployment-Vni46k3t.d.mts +137 -0
- package/dist/safe-deployment-Vni46k3t.d.mts.map +1 -0
- package/dist/signer-oaYGfjDe.d.mts +142 -0
- package/dist/signer-oaYGfjDe.d.mts.map +1 -0
- package/package.json +37 -70
- package/CHANGELOG.md +0 -274
- package/LICENSE +0 -44
- package/dist/client-CbUeJoM9.d.ts +0 -1335
- package/dist/client-UEm2oZbZ.d.cts +0 -1335
- package/dist/client.cjs +0 -4042
- package/dist/client.d.cts +0 -6
- package/dist/client.d.ts +0 -6
- package/dist/client.js +0 -4040
- package/dist/errors-CwhCWGxm.d.ts +0 -70
- package/dist/errors-rqxuUhQP.d.cts +0 -70
- package/dist/errors.cjs +0 -35
- package/dist/errors.d.cts +0 -2
- package/dist/errors.d.ts +0 -2
- package/dist/errors.js +0 -31
- package/dist/index.cjs +0 -4273
- package/dist/index.d.cts +0 -461
- package/dist/index.d.ts +0 -461
- package/dist/index.js +0 -4234
- package/dist/next-action-CTGl8wpy.d.cts +0 -177
- package/dist/next-action-CTGl8wpy.d.ts +0 -177
- package/dist/types-BD4VAQb5.d.ts +0 -1205
- package/dist/types-HlwCIjgQ.d.cts +0 -1205
- package/dist/webhooks.cjs +0 -118
- package/dist/webhooks.d.cts +0 -33
- package/dist/webhooks.d.ts +0 -33
- package/dist/webhooks.js +0 -116
package/README.md
CHANGED
|
@@ -1,312 +1,8 @@
|
|
|
1
1
|
# @capxul/sdk
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
payments, manage organization treasuries, and orchestrate on-chain
|
|
5
|
-
flows backed by audited custody contracts.
|
|
3
|
+
Layer 0 SDK package. Stage 1 currently ships the `@capxul/sdk/env` subpath.
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
> versions. Pin to an exact version (`@capxul/sdk@0.1.0-alpha.0`) and
|
|
9
|
-
> read the changelog before upgrading.
|
|
5
|
+
See `docs/env.md` for the env Module contract.
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
pnpm add @capxul/sdk @capxul/sdk-react
|
|
15
|
-
# or: npm install @capxul/sdk @capxul/sdk-react
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
`@capxul/sdk` is the headless contract — flows, types, errors. The
|
|
19
|
-
React-flavored entry point lives in `@capxul/sdk-react`. Pick:
|
|
20
|
-
|
|
21
|
-
- **Browser apps (recommended).** Install both. Use
|
|
22
|
-
`<CapxulProvider config={{ mode: "publishable-key", publishableKey }}>`
|
|
23
|
-
from `@capxul/sdk-react` and the `useMe` / `useCapxulStatus` hooks.
|
|
24
|
-
See that package's README for the lazy-DX example.
|
|
25
|
-
- **Server / CLI / scripts.** Install only `@capxul/sdk`. Build a
|
|
26
|
-
`CapxulClient` directly with your own auth and Convex adapters
|
|
27
|
-
(see "Server-side construction" below).
|
|
28
|
-
|
|
29
|
-
## Two-layer trust
|
|
30
|
-
|
|
31
|
-
Capxul splits the platform into two layers with different licenses:
|
|
32
|
-
|
|
33
|
-
| Layer | What it does | License |
|
|
34
|
-
| ---------------------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
35
|
-
| Custody (on-chain) | Safe v1.4.1 + ERC-4337 modules — every contract that holds, transfers, or signs over user funds | Open source, audited, in the [Capxul GitHub repo](https://github.com/Xelmar-tech/Capxul/tree/main/packages/contracts) |
|
|
36
|
-
| Orchestration (this package) | Workflow logic, business rules, integration glue, the `/v1/*` HTTP client | Proprietary — see `LICENSE` |
|
|
37
|
-
|
|
38
|
-
You don't have to take our word for the parts that hold money — those
|
|
39
|
-
contracts are open and auditable. The parts that orchestrate workflows
|
|
40
|
-
on top are our product.
|
|
41
|
-
|
|
42
|
-
## Quickstart (server / CLI)
|
|
43
|
-
|
|
44
|
-
```ts
|
|
45
|
-
import { createCapxulClient, createLocalSigner, tryCatch } from "@capxul/sdk";
|
|
46
|
-
|
|
47
|
-
const signer = createLocalSigner(process.env.CAPXUL_PRIVATE_KEY!);
|
|
48
|
-
const capxul = createCapxulClient({
|
|
49
|
-
apiKey: process.env.CAPXUL_API_KEY!, // server-to-server partner key
|
|
50
|
-
signer,
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
const [err, payment] = await tryCatch(
|
|
54
|
-
capxul.payments.create({
|
|
55
|
-
to: { email: "vendor@example.com" },
|
|
56
|
-
amount: { value: "100", currency: "USD" },
|
|
57
|
-
}),
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
if (err) {
|
|
61
|
-
if (err.code === "INSUFFICIENT_BALANCE") {
|
|
62
|
-
// narrow `err.code` to route the UI / reply
|
|
63
|
-
}
|
|
64
|
-
throw err;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
console.log(payment.id);
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
## Quickstart (browser via React)
|
|
71
|
-
|
|
72
|
-
See [`@capxul/sdk-react`](https://www.npmjs.com/package/@capxul/sdk-react).
|
|
73
|
-
The browser path uses a publishable key (`cap_pk_…`) and bootstraps
|
|
74
|
-
runtime URLs lazily — no need to ship secrets to the client:
|
|
75
|
-
|
|
76
|
-
```tsx
|
|
77
|
-
import { CapxulProvider, useMe, useCapxulStatus } from "@capxul/sdk-react";
|
|
78
|
-
|
|
79
|
-
<CapxulProvider
|
|
80
|
-
config={{
|
|
81
|
-
mode: "publishable-key",
|
|
82
|
-
publishableKey: process.env.NEXT_PUBLIC_CAPXUL_PUBLISHABLE_KEY!,
|
|
83
|
-
}}
|
|
84
|
-
>
|
|
85
|
-
<App />
|
|
86
|
-
</CapxulProvider>;
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
## Publishable-key transport
|
|
90
|
-
|
|
91
|
-
The browser transport accepts a publishable key, then lazily resolves
|
|
92
|
-
runtime URLs by POSTing to `/v1/client/bootstrap` on the first auth or
|
|
93
|
-
`ensureRuntime()` call. The successful backend response is intentionally
|
|
94
|
-
small:
|
|
95
|
-
|
|
96
|
-
```json
|
|
97
|
-
{
|
|
98
|
-
"authBaseUrl": "https://<deployment>.convex.site/api/auth",
|
|
99
|
-
"convexUrl": "https://<deployment>.convex.cloud"
|
|
100
|
-
}
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
The resolved runtime is cached for the lifetime of the transport. Concurrent
|
|
104
|
-
callers share one bootstrap request, successful resolutions are reused, and
|
|
105
|
-
failed bootstrap attempts reset so the next call can retry. Tests and
|
|
106
|
-
non-default deployments can inject both `fetchImpl` and an absolute
|
|
107
|
-
`bootstrapUrl`:
|
|
108
|
-
|
|
109
|
-
```ts
|
|
110
|
-
import { makeHttpTransport, CapxulError } from "@capxul/sdk";
|
|
111
|
-
|
|
112
|
-
const transport = makeHttpTransport({
|
|
113
|
-
mode: "publishable-key",
|
|
114
|
-
publishableKey: process.env.NEXT_PUBLIC_CAPXUL_PUBLISHABLE_KEY!,
|
|
115
|
-
bootstrapUrl: "https://api.capxul.com/v1/client/bootstrap",
|
|
116
|
-
fetchImpl: fetch,
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
await transport.ensureRuntime();
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
Local setup errors and backend bootstrap errors both use `CapxulError`, but
|
|
123
|
-
carry different `details.source` values:
|
|
124
|
-
|
|
125
|
-
```ts
|
|
126
|
-
try {
|
|
127
|
-
await transport.ensureRuntime();
|
|
128
|
-
} catch (error) {
|
|
129
|
-
if (error instanceof CapxulError) {
|
|
130
|
-
if (error.details?.source === "sdk-config") {
|
|
131
|
-
// Missing or malformed local config, such as publishableKey or bootstrapUrl.
|
|
132
|
-
}
|
|
133
|
-
if (error.details?.source === "backend-bootstrap") {
|
|
134
|
-
// Sanitized backend refusal, such as NOT_AUTHENTICATED or PERMISSION_DENIED.
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
Runtime proof status:
|
|
141
|
-
|
|
142
|
-
| Surface | Source support | Runtime proof | Status |
|
|
143
|
-
| -------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
|
|
144
|
-
| SDK transport | `makeHttpTransport({ mode: "publishable-key", publishableKey })` | `packages/sdk/tests/unit/transport.test.ts` proves config validation, singleflight bootstrap, retry after failure, lifecycle transitions, and sanitized backend errors. | Proven with mocked fetch |
|
|
145
|
-
| React provider | `CapxulProvider config={{ mode: "publishable-key", ... }}` | `packages/sdk-react/ops/proof/react-headless.test.tsx` proves bootstrap before a real `useMe()` read through the provider and lazy Convex data client. | Proven with mocked fetch + headless React |
|
|
146
|
-
| Reference CLI | `bootstrap probe --mock --json` | `apps/reference-cli/scripts/agent-driver.ts` phase 0 and the direct CLI command prove provider/bootstrap/auth ordering and sanitized output. | Proven locally; live endpoint remains manual-key gated |
|
|
147
|
-
|
|
148
|
-
Copy-paste local replication:
|
|
149
|
-
|
|
150
|
-
```bash
|
|
151
|
-
corepack pnpm --filter @capxul/sdk check-types
|
|
152
|
-
corepack pnpm --filter @capxul/sdk build
|
|
153
|
-
corepack pnpm --filter @capxul/sdk-react check-types
|
|
154
|
-
corepack pnpm --filter @capxul/sdk-react build
|
|
155
|
-
corepack pnpm --filter @capxul/reference-cli check-types
|
|
156
|
-
corepack pnpm --filter @capxul/reference-cli build
|
|
157
|
-
node apps/reference-cli/dist/cli.js bootstrap probe --mock --json
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
Run the SDK and React SDK builds before the reference CLI typecheck in a
|
|
161
|
-
fresh checkout; the CLI depends on their generated declaration outputs.
|
|
162
|
-
|
|
163
|
-
Expected sanitized pass signal:
|
|
164
|
-
|
|
165
|
-
```json
|
|
166
|
-
{
|
|
167
|
-
"command": "bootstrap.probe",
|
|
168
|
-
"ok": true,
|
|
169
|
-
"mode": "publishable-key",
|
|
170
|
-
"status": "ready",
|
|
171
|
-
"bootstrapRequests": 1,
|
|
172
|
-
"authRequests": 1,
|
|
173
|
-
"keyLengthClass": "provided",
|
|
174
|
-
"mocked": true
|
|
175
|
-
}
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
## Public surface (alpha)
|
|
179
|
-
|
|
180
|
-
```ts
|
|
181
|
-
capxul.auth // sendOtp, verifyOtp, signOut, serviceTokenMint
|
|
182
|
-
capxul.me // get, update — first-party caller identity
|
|
183
|
-
capxul.accounts // create, retrieve, list
|
|
184
|
-
capxul.organizations // CRUD + members + payments + treasury
|
|
185
|
-
capxul.payments // create, retrieve, list
|
|
186
|
-
capxul.withdrawals // create, retrieve, list
|
|
187
|
-
capxul.documents // personal invoices first; broader document variants typed
|
|
188
|
-
capxul.flows.{auth, onboarding, provisioning} // XState v5 flows
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
### Personal invoice documents
|
|
192
|
-
|
|
193
|
-
The first `documents` runtime surface is the personal invoice pilot.
|
|
194
|
-
It creates immutable open invoice documents with a canonical
|
|
195
|
-
`invoiceHash`. The backend stores the private invoice payload; the
|
|
196
|
-
chain receives only the hash as `documentHash`.
|
|
197
|
-
|
|
198
|
-
```ts
|
|
199
|
-
const [err, invoice] = await capxul.documents.create({
|
|
200
|
-
type: "invoice",
|
|
201
|
-
recipient: { email: "client@example.com" },
|
|
202
|
-
amount: { value: "25.00", currency: "USD" },
|
|
203
|
-
lineItems: [
|
|
204
|
-
{ description: "Design", quantity: 1, unitPrice: "25.00", currency: "USD" },
|
|
205
|
-
],
|
|
206
|
-
reference: "May design work",
|
|
207
|
-
dueAt: "2026-05-20T00:00:00.000Z",
|
|
208
|
-
});
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
Use `capxul.documents.retrieve(docId)`, `.list({ type: "invoice" })`,
|
|
212
|
-
and `.cancel(docId)` for the rest of the pilot surface. Org-admin
|
|
213
|
-
invoices, file uploads, PDFs, partial payments, and mutable open
|
|
214
|
-
invoices are outside this pilot.
|
|
215
|
-
|
|
216
|
-
Helpers:
|
|
217
|
-
|
|
218
|
-
- `tryCatch(promise)` — error tuple `[error, data]`; routes on
|
|
219
|
-
`error.code` (typed `CapxulErrorCode` union)
|
|
220
|
-
- `matchError`, `matchStatus`, `matchAction` — exhaustive pattern
|
|
221
|
-
matchers per CANON.md §4.44
|
|
222
|
-
- Branded type constructors — `toEmail`, `toAccountId`,
|
|
223
|
-
`toOrganizationId`, … — never cast raw strings
|
|
224
|
-
|
|
225
|
-
## Organizations
|
|
226
|
-
|
|
227
|
-
Create an organization, manage its treasury, and invite members with
|
|
228
|
-
role-based access:
|
|
229
|
-
|
|
230
|
-
```ts
|
|
231
|
-
// Create
|
|
232
|
-
const [err, org] = await capxul.organizations.create({ name: "Acme" });
|
|
233
|
-
|
|
234
|
-
// Invite a member — returns the raw token once
|
|
235
|
-
const [err, { member, inviteToken }] =
|
|
236
|
-
await capxul.organizations.members.invite({
|
|
237
|
-
organizationId: org.id,
|
|
238
|
-
email: "alice@example.com",
|
|
239
|
-
role: "finance_manager",
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
// List members — defaults to active; pass status to filter
|
|
243
|
-
const [err, list] = await capxul.organizations.members.list({
|
|
244
|
-
organizationId: org.id,
|
|
245
|
-
status: "pending", // "pending" | "active" | "revoked" | "expired" | "all"
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
// Accept an invitation (called by the invitee)
|
|
249
|
-
const [err, member] = await capxul.organizations.members.accept({
|
|
250
|
-
token: inviteToken,
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
// Revoke a pending invitation
|
|
254
|
-
const [err] = await capxul.organizations.members.revoke({
|
|
255
|
-
organizationId: org.id,
|
|
256
|
-
memberId: member.id,
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
// Remove an active member
|
|
260
|
-
const [err] = await capxul.organizations.members.remove({
|
|
261
|
-
organizationId: org.id,
|
|
262
|
-
memberId: member.id,
|
|
263
|
-
});
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
Member roles: `owner`, `finance_manager`, `payments_operator`,
|
|
267
|
-
`org_admin`, `viewer`, `accountant`. Only `owner` and `org_admin` can
|
|
268
|
-
invite, revoke, remove, or change roles.
|
|
269
|
-
|
|
270
|
-
Pending invitations auto-accept on first sign-in when the user's email
|
|
271
|
-
matches. Overdue pending invitations expire via a nightly cron.
|
|
272
|
-
|
|
273
|
-
## Error model
|
|
274
|
-
|
|
275
|
-
Every method returns a `CapxulResult<T, Codes>` tuple. Errors are
|
|
276
|
-
instances of `CapxulError` with a typed `code` (narrowed per method).
|
|
277
|
-
|
|
278
|
-
```ts
|
|
279
|
-
const [err, account] = await tryCatch(capxul.me.get());
|
|
280
|
-
|
|
281
|
-
if (err) {
|
|
282
|
-
switch (err.code) {
|
|
283
|
-
case "PROFILE_NOT_FOUND":
|
|
284
|
-
return router.push("/onboarding");
|
|
285
|
-
case "NOT_AUTHENTICATED":
|
|
286
|
-
return router.push("/login");
|
|
287
|
-
default:
|
|
288
|
-
throw err; // unhandled — bubble to the error boundary
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
There is no second error type. Backend `CapxulError` → Convex
|
|
294
|
-
serialization → SDK deserialization → frontend `CapxulError` — one
|
|
295
|
-
class, 34 codes, end-to-end.
|
|
296
|
-
|
|
297
|
-
## Telemetry (opt-out)
|
|
298
|
-
|
|
299
|
-
The SDK reports operational telemetry (operation IDs, latencies,
|
|
300
|
-
correlation IDs, error codes) to Capxul. **PII is never sent** — events
|
|
301
|
-
are scrubbed at the source. Disable by passing
|
|
302
|
-
`{ telemetry: { enabled: false } }` (coming in `0.2.0`); current alpha
|
|
303
|
-
sends only operational signals which Capxul Terms disclose explicitly.
|
|
304
|
-
|
|
305
|
-
## License
|
|
306
|
-
|
|
307
|
-
Proprietary — see [`LICENSE`](./LICENSE). The orchestration code in
|
|
308
|
-
this package is governed by the Capxul Terms of Service. The
|
|
309
|
-
value-bearing custody contracts are open source — see "Two-layer trust"
|
|
310
|
-
above.
|
|
311
|
-
|
|
312
|
-
Contact: [legal@capxul.com](mailto:legal@capxul.com)
|
|
7
|
+
See `docs/identity-adapters.md` for the Effect-native identity port and adapter
|
|
8
|
+
Layer exports.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Context, Data, Effect, Layer } from "effect";
|
|
2
|
+
import { toAuthUserId, toEmail, toEpochMs, toEpochSeconds, toJwtToken, toSessionToken } from "@capxul/types";
|
|
3
|
+
//#region src/ports/auth-cache.ts
|
|
4
|
+
var AuthCacheError = class extends Data.TaggedError("AuthCacheError") {};
|
|
5
|
+
var AuthCachePortTag = class extends Context.Tag("@capxul/sdk/ports/AuthCachePort")() {};
|
|
6
|
+
//#endregion
|
|
7
|
+
//#region src/adapters/auth-cache/serialization.ts
|
|
8
|
+
function parseAuthSession(raw) {
|
|
9
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
10
|
+
const candidate = raw;
|
|
11
|
+
try {
|
|
12
|
+
return {
|
|
13
|
+
authUserId: toAuthUserId(candidate.authUserId),
|
|
14
|
+
email: toEmail(candidate.email),
|
|
15
|
+
token: toSessionToken(candidate.token),
|
|
16
|
+
expiresAt: toEpochMs(candidate.expiresAt)
|
|
17
|
+
};
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function parseCachedJwt(raw) {
|
|
23
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
24
|
+
const candidate = raw;
|
|
25
|
+
try {
|
|
26
|
+
return {
|
|
27
|
+
token: toJwtToken(candidate.token),
|
|
28
|
+
expEpochSeconds: toEpochSeconds(candidate.expEpochSeconds)
|
|
29
|
+
};
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/adapters/auth-cache/BrowserAuthCacheAdapter.ts
|
|
36
|
+
const SESSION_KEY = "capxul.session";
|
|
37
|
+
const JWT_KEY = "capxul.jwt";
|
|
38
|
+
var BrowserAuthCacheAdapter = class {
|
|
39
|
+
storage;
|
|
40
|
+
constructor(storage) {
|
|
41
|
+
this.storage = storage;
|
|
42
|
+
}
|
|
43
|
+
getSession = authCacheTry("getSession", () => {
|
|
44
|
+
const raw = this.storage.getItem(SESSION_KEY);
|
|
45
|
+
if (raw === null) return null;
|
|
46
|
+
try {
|
|
47
|
+
return parseAuthSession(JSON.parse(raw));
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
setSession = (session) => authCacheTry("setSession", () => {
|
|
53
|
+
this.storage.setItem(SESSION_KEY, JSON.stringify(session));
|
|
54
|
+
});
|
|
55
|
+
clearSession = authCacheTry("clearSession", () => {
|
|
56
|
+
this.storage.removeItem(SESSION_KEY);
|
|
57
|
+
});
|
|
58
|
+
getJwt = authCacheTry("getJwt", () => {
|
|
59
|
+
const raw = this.storage.getItem(JWT_KEY);
|
|
60
|
+
if (raw === null) return null;
|
|
61
|
+
try {
|
|
62
|
+
return parseCachedJwt(JSON.parse(raw));
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
setJwt = (jwt) => authCacheTry("setJwt", () => {
|
|
68
|
+
this.storage.setItem(JWT_KEY, JSON.stringify(jwt));
|
|
69
|
+
});
|
|
70
|
+
clearJwt = authCacheTry("clearJwt", () => {
|
|
71
|
+
this.storage.removeItem(JWT_KEY);
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
function toAuthCacheError(operation, cause) {
|
|
75
|
+
return new AuthCacheError({
|
|
76
|
+
operation,
|
|
77
|
+
cause
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function authCacheTry(operation, run) {
|
|
81
|
+
return Effect.try({
|
|
82
|
+
try: run,
|
|
83
|
+
catch: (cause) => toAuthCacheError(operation, cause)
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/adapters/auth-cache/InMemoryAuthCacheAdapter.ts
|
|
88
|
+
var InMemoryAuthCacheAdapter = class {
|
|
89
|
+
session = null;
|
|
90
|
+
jwt = null;
|
|
91
|
+
getSession = Effect.sync(() => this.session);
|
|
92
|
+
setSession = (session) => Effect.sync(() => {
|
|
93
|
+
this.session = session;
|
|
94
|
+
});
|
|
95
|
+
clearSession = Effect.sync(() => {
|
|
96
|
+
this.session = null;
|
|
97
|
+
});
|
|
98
|
+
getJwt = Effect.sync(() => this.jwt);
|
|
99
|
+
setJwt = (jwt) => Effect.sync(() => {
|
|
100
|
+
this.jwt = jwt;
|
|
101
|
+
});
|
|
102
|
+
clearJwt = Effect.sync(() => {
|
|
103
|
+
this.jwt = null;
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
Layer.effect(AuthCachePortTag, Effect.sync(() => new InMemoryAuthCacheAdapter()).pipe(Effect.mapError((cause) => new AuthCacheError({
|
|
107
|
+
operation: "initialize",
|
|
108
|
+
cause
|
|
109
|
+
}))));
|
|
110
|
+
//#endregion
|
|
111
|
+
export { AuthCacheError as a, parseCachedJwt as i, BrowserAuthCacheAdapter as n, AuthCachePortTag as o, parseAuthSession as r, InMemoryAuthCacheAdapter as t };
|
|
112
|
+
|
|
113
|
+
//# sourceMappingURL=InMemoryAuthCacheAdapter-BK-B_ERB.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InMemoryAuthCacheAdapter-BK-B_ERB.mjs","names":[],"sources":["../src/ports/auth-cache.ts","../src/adapters/auth-cache/serialization.ts","../src/adapters/auth-cache/BrowserAuthCacheAdapter.ts","../src/adapters/auth-cache/InMemoryAuthCacheAdapter.ts"],"sourcesContent":["import { Context, Data, Effect } from \"effect\";\nimport type { AuthSession, EpochSeconds, JwtToken } from \"@capxul/types\";\n\n/**\n * AuthCachePort (TA4) — replaces `SessionStoragePort` in the rebuild slice's\n * consumer-facing wiring. Stores the user-snapshot Session AND the cached\n * Convex JWT (W7 bridge endpoint). The two slots are independent so a session\n * refresh doesn't invalidate the JWT and vice versa.\n *\n * Three adapters (TA7):\n * - `BrowserAuthCacheAdapter` — backed by `localStorage`\n * - `FileSystemAuthCacheAdapter` — mode-0600 JSON file in `~/.config/capxul/`\n * - `InMemoryAuthCacheAdapter` — for tests\n *\n * `SessionStoragePort` was retired by the Stage 4 rebuild; session and JWT\n * persistence now share this cache boundary.\n */\nexport interface AuthCachePort {\n readonly getSession: Effect.Effect<AuthSession | null, AuthCacheError>;\n readonly setSession: (session: AuthSession) => Effect.Effect<void, AuthCacheError>;\n readonly clearSession: Effect.Effect<void, AuthCacheError>;\n\n /**\n * JWT cache for the `/api/auth/convex/token` bridge endpoint (W7).\n * Stored separately from the session so a session refresh doesn't\n * invalidate cached JWT.\n */\n readonly getJwt: Effect.Effect<CachedJwt | null, AuthCacheError>;\n readonly setJwt: (jwt: CachedJwt) => Effect.Effect<void, AuthCacheError>;\n readonly clearJwt: Effect.Effect<void, AuthCacheError>;\n}\n\n/**\n * Cached Convex JWT shape (TA4). `expEpochSeconds` is decoded from the JWT's\n * `exp` claim at fetch time so the `tokenProvider` cache eviction logic can\n * proactively refresh at `exp - 30s` per TA3.\n */\nexport interface CachedJwt {\n readonly token: JwtToken;\n readonly expEpochSeconds: EpochSeconds;\n}\n\nexport class AuthCacheError extends Data.TaggedError(\"AuthCacheError\")<{\n readonly operation: string;\n readonly cause: unknown;\n}> {}\n\nexport class AuthCachePortTag extends Context.Tag(\"@capxul/sdk/ports/AuthCachePort\")<\n AuthCachePortTag,\n AuthCachePort\n>() {}\n","import {\n toAuthUserId,\n toEmail,\n toEpochMs,\n toEpochSeconds,\n toJwtToken,\n toSessionToken,\n type AuthSession,\n} from \"@capxul/types\";\n\nimport type { CachedJwt } from \"../../ports/auth-cache\";\n\nexport function parseAuthSession(raw: unknown): AuthSession | null {\n if (typeof raw !== \"object\" || raw === null) return null;\n const candidate = raw as {\n readonly authUserId?: unknown;\n readonly email?: unknown;\n readonly token?: unknown;\n readonly expiresAt?: unknown;\n };\n\n try {\n return {\n authUserId: toAuthUserId(candidate.authUserId),\n email: toEmail(candidate.email),\n token: toSessionToken(candidate.token),\n expiresAt: toEpochMs(candidate.expiresAt),\n };\n } catch {\n return null;\n }\n}\n\nexport function parseCachedJwt(raw: unknown): CachedJwt | null {\n if (typeof raw !== \"object\" || raw === null) return null;\n const candidate = raw as {\n readonly token?: unknown;\n readonly expEpochSeconds?: unknown;\n };\n\n try {\n return {\n token: toJwtToken(candidate.token),\n expEpochSeconds: toEpochSeconds(candidate.expEpochSeconds),\n };\n } catch {\n return null;\n }\n}\n","import { Effect, Layer } from \"effect\";\nimport type { AuthSession } from \"@capxul/types\";\n\nimport {\n AuthCacheError,\n AuthCachePortTag,\n type AuthCachePort,\n type CachedJwt,\n} from \"../../ports/auth-cache\";\nimport { parseAuthSession, parseCachedJwt } from \"./serialization\";\n\nexport interface BrowserStorageShape {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\nconst SESSION_KEY = \"capxul.session\";\nconst JWT_KEY = \"capxul.jwt\";\n\nexport class BrowserAuthCacheAdapter implements AuthCachePort {\n private readonly storage: BrowserStorageShape;\n\n constructor(storage: BrowserStorageShape) {\n this.storage = storage;\n }\n\n readonly getSession = authCacheTry(\"getSession\", () => {\n const raw = this.storage.getItem(SESSION_KEY);\n if (raw === null) return null;\n try {\n return parseAuthSession(JSON.parse(raw));\n } catch {\n return null;\n }\n });\n\n readonly setSession = (session: AuthSession) =>\n authCacheTry(\"setSession\", () => {\n this.storage.setItem(SESSION_KEY, JSON.stringify(session));\n });\n\n readonly clearSession = authCacheTry(\"clearSession\", () => {\n this.storage.removeItem(SESSION_KEY);\n });\n\n readonly getJwt = authCacheTry(\"getJwt\", () => {\n const raw = this.storage.getItem(JWT_KEY);\n if (raw === null) return null;\n try {\n return parseCachedJwt(JSON.parse(raw));\n } catch {\n return null;\n }\n });\n\n readonly setJwt = (jwt: CachedJwt) =>\n authCacheTry(\"setJwt\", () => {\n this.storage.setItem(JWT_KEY, JSON.stringify(jwt));\n });\n\n readonly clearJwt = authCacheTry(\"clearJwt\", () => {\n this.storage.removeItem(JWT_KEY);\n });\n}\n\nexport function BrowserAuthCacheLayer(input: {\n readonly storage: BrowserStorageShape;\n}): Layer.Layer<AuthCachePortTag, AuthCacheError> {\n return Layer.effect(\n AuthCachePortTag,\n Effect.sync(() => new BrowserAuthCacheAdapter(input.storage)).pipe(\n Effect.mapError((cause) => toAuthCacheError(\"initialize\", cause)),\n ),\n );\n}\n\nfunction toAuthCacheError(operation: string, cause: unknown): AuthCacheError {\n return new AuthCacheError({ operation, cause });\n}\n\nfunction authCacheTry<T>(operation: string, run: () => T): Effect.Effect<T, AuthCacheError> {\n return Effect.try({\n try: run,\n catch: (cause) => toAuthCacheError(operation, cause),\n });\n}\n","import { Effect, Layer } from \"effect\";\nimport type { AuthSession } from \"@capxul/types\";\n\nimport {\n AuthCacheError,\n AuthCachePortTag,\n type AuthCachePort,\n type CachedJwt,\n} from \"../../ports/auth-cache\";\n\nexport class InMemoryAuthCacheAdapter implements AuthCachePort {\n private session: AuthSession | null = null;\n private jwt: CachedJwt | null = null;\n\n readonly getSession = Effect.sync(() => this.session);\n\n readonly setSession = (session: AuthSession) =>\n Effect.sync(() => {\n this.session = session;\n });\n\n readonly clearSession = Effect.sync(() => {\n this.session = null;\n });\n\n readonly getJwt = Effect.sync(() => this.jwt);\n\n readonly setJwt = (jwt: CachedJwt) =>\n Effect.sync(() => {\n this.jwt = jwt;\n });\n\n readonly clearJwt = Effect.sync(() => {\n this.jwt = null;\n });\n}\n\nexport const InMemoryAuthCacheLayer = Layer.effect(\n AuthCachePortTag,\n Effect.sync(() => new InMemoryAuthCacheAdapter()).pipe(\n Effect.mapError((cause) => new AuthCacheError({ operation: \"initialize\", cause })),\n ),\n);\n"],"mappings":";;;AA0CA,IAAa,iBAAb,cAAoC,KAAK,YAAY,gBAAgB,EAGlE,CAAC;AAEJ,IAAa,mBAAb,cAAsC,QAAQ,IAAI,iCAAiC,EAGjF,EAAE,CAAC;;;ACtCL,SAAgB,iBAAiB,KAAkC;CACjE,IAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM,OAAO;CACpD,MAAM,YAAY;CAOlB,IAAI;EACF,OAAO;GACL,YAAY,aAAa,UAAU,UAAU;GAC7C,OAAO,QAAQ,UAAU,KAAK;GAC9B,OAAO,eAAe,UAAU,KAAK;GACrC,WAAW,UAAU,UAAU,SAAS;EAC1C;CACF,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAgB,eAAe,KAAgC;CAC7D,IAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM,OAAO;CACpD,MAAM,YAAY;CAKlB,IAAI;EACF,OAAO;GACL,OAAO,WAAW,UAAU,KAAK;GACjC,iBAAiB,eAAe,UAAU,eAAe;EAC3D;CACF,QAAQ;EACN,OAAO;CACT;AACF;;;AC/BA,MAAM,cAAc;AACpB,MAAM,UAAU;AAEhB,IAAa,0BAAb,MAA8D;CAC5D;CAEA,YAAY,SAA8B;EACxC,KAAK,UAAU;CACjB;CAEA,aAAsB,aAAa,oBAAoB;EACrD,MAAM,MAAM,KAAK,QAAQ,QAAQ,WAAW;EAC5C,IAAI,QAAQ,MAAM,OAAO;EACzB,IAAI;GACF,OAAO,iBAAiB,KAAK,MAAM,GAAG,CAAC;EACzC,QAAQ;GACN,OAAO;EACT;CACF,CAAC;CAED,cAAuB,YACrB,aAAa,oBAAoB;EAC/B,KAAK,QAAQ,QAAQ,aAAa,KAAK,UAAU,OAAO,CAAC;CAC3D,CAAC;CAEH,eAAwB,aAAa,sBAAsB;EACzD,KAAK,QAAQ,WAAW,WAAW;CACrC,CAAC;CAED,SAAkB,aAAa,gBAAgB;EAC7C,MAAM,MAAM,KAAK,QAAQ,QAAQ,OAAO;EACxC,IAAI,QAAQ,MAAM,OAAO;EACzB,IAAI;GACF,OAAO,eAAe,KAAK,MAAM,GAAG,CAAC;EACvC,QAAQ;GACN,OAAO;EACT;CACF,CAAC;CAED,UAAmB,QACjB,aAAa,gBAAgB;EAC3B,KAAK,QAAQ,QAAQ,SAAS,KAAK,UAAU,GAAG,CAAC;CACnD,CAAC;CAEH,WAAoB,aAAa,kBAAkB;EACjD,KAAK,QAAQ,WAAW,OAAO;CACjC,CAAC;AACH;AAaA,SAAS,iBAAiB,WAAmB,OAAgC;CAC3E,OAAO,IAAI,eAAe;EAAE;EAAW;CAAM,CAAC;AAChD;AAEA,SAAS,aAAgB,WAAmB,KAAgD;CAC1F,OAAO,OAAO,IAAI;EAChB,KAAK;EACL,QAAQ,UAAU,iBAAiB,WAAW,KAAK;CACrD,CAAC;AACH;;;AC5EA,IAAa,2BAAb,MAA+D;CAC7D,UAAsC;CACtC,MAAgC;CAEhC,aAAsB,OAAO,WAAW,KAAK,OAAO;CAEpD,cAAuB,YACrB,OAAO,WAAW;EAChB,KAAK,UAAU;CACjB,CAAC;CAEH,eAAwB,OAAO,WAAW;EACxC,KAAK,UAAU;CACjB,CAAC;CAED,SAAkB,OAAO,WAAW,KAAK,GAAG;CAE5C,UAAmB,QACjB,OAAO,WAAW;EAChB,KAAK,MAAM;CACb,CAAC;CAEH,WAAoB,OAAO,WAAW;EACpC,KAAK,MAAM;CACb,CAAC;AACH;AAEsC,MAAM,OAC1C,kBACA,OAAO,WAAW,IAAI,yBAAyB,CAAC,EAAE,KAChD,OAAO,UAAU,UAAU,IAAI,eAAe;CAAE,WAAW;CAAc;AAAM,CAAC,CAAC,CACnF,CACF"}
|