@tangle-network/agent-app 0.3.1 → 0.5.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.
@@ -0,0 +1,339 @@
1
+ import { PlatformIdentity, PlatformBillingClient } from '../billing/index.js';
2
+
3
+ /**
4
+ * Cross-site Tangle SSO for agent apps: signed-state CSRF cookies plus the
5
+ * full start/callback orchestration against the platform's /cross-site
6
+ * bridge. The platform wire client and account persistence are structural
7
+ * seams (`TangleSsoAuthClient` / `TangleSsoAccountStore`), so this module
8
+ * never imports agent-runtime, an auth framework, or a database driver.
9
+ * WebCrypto only — runs in workerd without node compatibility flags.
10
+ */
11
+ interface SsoStateConfig {
12
+ /** HMAC-SHA256 secret (e.g. the app's auth secret). */
13
+ secret: string;
14
+ /** State lifetime in ms. Default 600 000. */
15
+ ttlMs?: number;
16
+ /** Injectable clock (ms since epoch). Default Date.now. */
17
+ now?: () => number;
18
+ }
19
+ /** Mint a `<randomHex32>.<timestamp36>.<hmacHex>` state value. The timestamp
20
+ * is inside the signed payload, so expiry survives cookie-attribute tampering. */
21
+ declare function createSignedSsoState(config: SsoStateConfig): Promise<string>;
22
+ /** Verify the MAC (constant-time) and the signed TTL. */
23
+ declare function verifySignedSsoState(state: string, config: SsoStateConfig): Promise<boolean>;
24
+ interface TangleSsoExchangeResult {
25
+ apiKey: string;
26
+ user: {
27
+ id: string;
28
+ email: string;
29
+ name?: string | null;
30
+ };
31
+ plan?: {
32
+ tier: string;
33
+ } | null;
34
+ }
35
+ /** Structural mirror of the platform auth wire client — any object with these
36
+ * two methods satisfies it without this module importing the concrete class. */
37
+ interface TangleSsoAuthClient {
38
+ authorizeUrl(options: {
39
+ state: string;
40
+ redirectUri?: string;
41
+ }): string;
42
+ exchange(code: string): Promise<TangleSsoExchangeResult>;
43
+ }
44
+ /** Thrown by `upsertUserByEmail` when the app-local user row cannot be
45
+ * created; the callback handler maps it to `?error=tangle_user_create_failed`.
46
+ * Any other store error propagates. */
47
+ declare class TangleSsoUserCreateError extends Error {
48
+ constructor(message?: string);
49
+ }
50
+ /**
51
+ * Account persistence seam. Covers both storage styles in use: link-table
52
+ * apps (a per-user platform-link row) and session-column apps (the key on the
53
+ * session row) — `saveTangleLink` receives both `userId` and `sessionToken`,
54
+ * and each app persists with the key it needs. `createSession` runs first so
55
+ * the token is always available to `saveTangleLink`.
56
+ */
57
+ interface TangleSsoAccountStore {
58
+ /** Find-or-create the app-local user for the platform email. */
59
+ upsertUserByEmail(input: {
60
+ email: string;
61
+ name: string | null;
62
+ }): Promise<{
63
+ userId: string;
64
+ }>;
65
+ /** Create an app session row; returns the session-cookie token value. */
66
+ createSession(input: {
67
+ userId: string;
68
+ expiresAt: Date;
69
+ ipAddress: string | null;
70
+ userAgent: string | null;
71
+ }): Promise<{
72
+ token: string;
73
+ }>;
74
+ /** Persist the platform link (API key + platform identity). */
75
+ saveTangleLink(input: {
76
+ userId: string;
77
+ sessionToken: string;
78
+ tangleUserId: string;
79
+ email: string;
80
+ name: string | null;
81
+ apiKey: string;
82
+ planTier: string | null;
83
+ }): Promise<void>;
84
+ }
85
+ interface TangleSsoHandlerOptions {
86
+ auth: TangleSsoAuthClient;
87
+ store: TangleSsoAccountStore;
88
+ /** HMAC secret for the state cookie. */
89
+ stateSecret: string;
90
+ /** Absolute callback URL registered with the platform. */
91
+ callbackUrl: string;
92
+ stateCookieName: string;
93
+ /** Default 'better-auth.session_token'. */
94
+ sessionCookieName?: string;
95
+ /** Adds `Secure` to every cookie this module sets. */
96
+ secureCookies: boolean;
97
+ /** Default 604 800 (7 days). */
98
+ sessionTtlSeconds?: number;
99
+ /** Default 600. Applies to both the cookie Max-Age and the signed TTL. */
100
+ stateTtlSeconds?: number;
101
+ /** Default '/app'. */
102
+ defaultRedirectPath?: string;
103
+ /** Default '/login'. */
104
+ loginPath?: string;
105
+ /** Failure log hook (e.g. console.error). Default no-op. */
106
+ log?: (message: string, error?: unknown) => void;
107
+ now?: () => number;
108
+ }
109
+ interface TangleSsoHandlers {
110
+ /** GET start route: mint + sign state, set the state cookie, 302 to the
111
+ * platform authorize URL. `?redirect=` carries the post-login path. */
112
+ start(request: Request): Promise<Response>;
113
+ /** GET callback route: verify state, exchange the code, upsert the user,
114
+ * create the session, save the platform link, set the session cookie,
115
+ * 302 to the saved redirect. Every failure 302s to
116
+ * `loginPath?error=…` with the state cookie cleared. */
117
+ callback(request: Request): Promise<Response>;
118
+ }
119
+ declare function createTangleSsoHandlers(opts: TangleSsoHandlerOptions): TangleSsoHandlers;
120
+
121
+ /**
122
+ * Integrations-hub proxy routes: the app-side surface that forwards an
123
+ * authenticated user's requests to the platform's `/v1/integrations/*` API
124
+ * using their stored platform key. Auth, key lookup, and the wire client are
125
+ * structural seams (`HubProxyContext`); error detection is by name + shape so
126
+ * it survives bundlers duplicating module instances.
127
+ */
128
+ declare class TangleBearerMissingError extends Error {
129
+ readonly userId: string;
130
+ constructor(userId: string);
131
+ }
132
+ /** Structural guard (name + userId shape) — robust when the error class is
133
+ * constructed in a different module instance than the one checking it. */
134
+ declare function isTangleBearerMissingError(error: unknown): error is TangleBearerMissingError;
135
+ /** Structural detection of the platform hub wire error (name + numeric status). */
136
+ declare function isPlatformHubErrorLike(error: unknown): error is Error & {
137
+ status: number;
138
+ code?: string;
139
+ };
140
+ /** Structural subset of the platform hub wire client — extra methods are fine. */
141
+ interface HubClientLike {
142
+ catalog(): Promise<unknown>;
143
+ listConnections(): Promise<unknown>;
144
+ revokeConnection(connectionId: string): Promise<unknown>;
145
+ startAuth(input: {
146
+ providerId: string;
147
+ connectorId: string;
148
+ returnUrl: string;
149
+ requestedScopes?: string[];
150
+ }): Promise<{
151
+ authorizationUrl: string;
152
+ state: string;
153
+ }>;
154
+ listHealthchecks(): Promise<unknown>;
155
+ }
156
+ interface HubProxyContext {
157
+ /** Resolve the authenticated user id. Throw the app's own auth Response /
158
+ * redirect to reject — it propagates untouched. */
159
+ requireUserId(request: Request): Promise<string>;
160
+ /** The user's platform bearer; throw `TangleBearerMissingError` when unlinked. */
161
+ getBearer(userId: string): Promise<string>;
162
+ /** A hub client bound to the bearer. */
163
+ createHubClient(bearer: string): HubClientLike;
164
+ }
165
+ interface HubProxyRouteArgs {
166
+ request: Request;
167
+ params?: Record<string, string | undefined>;
168
+ }
169
+ interface HubProxyRoutes {
170
+ /** GET → `{ catalog }`. */
171
+ catalog(args: HubProxyRouteArgs): Promise<Response>;
172
+ /** GET → `{ connections }`. */
173
+ connections(args: HubProxyRouteArgs): Promise<Response>;
174
+ /** DELETE → the platform revocation result verbatim; 405 otherwise. */
175
+ connectionDelete(args: {
176
+ request: Request;
177
+ params: {
178
+ connectionId: string;
179
+ };
180
+ }): Promise<Response>;
181
+ /** GET → `{ healthchecks }`. */
182
+ healthchecks(args: HubProxyRouteArgs): Promise<Response>;
183
+ /** POST `{ providerId, connectorId, returnUrl, requestedScopes? }` →
184
+ * `{ authorizationUrl, state }`; 405 non-POST; 400 on bad JSON / missing fields. */
185
+ authStart(args: HubProxyRouteArgs): Promise<Response>;
186
+ }
187
+ declare function createHubProxyRoutes(ctx: HubProxyContext): HubProxyRoutes;
188
+
189
+ /**
190
+ * Platform billing HTTP transport + tier state for apps on the shared
191
+ * Tangle balance model (id.tangle.tools). Reads authenticate as the user via
192
+ * their per-user platform key (the platform resolves the caller from the
193
+ * key; service or impersonation headers on read routes are rejected). The
194
+ * deduct write authenticates as the product service (`Bearer <serviceToken>`
195
+ * + `X-Service-Name`) and names the target user in the body. Also provides a
196
+ * fetch-backed implementation of the `/billing` module's
197
+ * `PlatformBillingClient` seam (type-only import — no runtime coupling).
198
+ */
199
+
200
+ type TanglePlanTier = 'free' | 'pro' | 'enterprise';
201
+ /** 'pro' | 'enterprise' pass through; anything else (null, unknown) → 'free'. */
202
+ declare function normalizeTanglePlanTier(plan: string | null | undefined): TanglePlanTier;
203
+ declare class PlatformBillingHttpError extends Error {
204
+ readonly status: number;
205
+ constructor(status: number, detail: string);
206
+ }
207
+ /** Structural guard (name + numeric status) — robust across module instances. */
208
+ declare function isPlatformBillingHttpError(error: unknown): error is PlatformBillingHttpError;
209
+ interface PlatformBillingHttpOptions {
210
+ /** Platform root, e.g. https://id.tangle.tools (trailing slashes stripped). */
211
+ baseUrl: string;
212
+ /** Used only by `deduct()`; resolved lazily so reads never require it.
213
+ * Throws at call time when empty. */
214
+ serviceToken: string | (() => string);
215
+ /** Product slug — the `X-Service-Name` header and the deduct `product` field. */
216
+ productSlug: string;
217
+ fetchImpl?: typeof fetch;
218
+ /** Default 10 000. */
219
+ timeoutMs?: number;
220
+ }
221
+ interface PlatformSubscriptionInfo {
222
+ tier: TanglePlanTier;
223
+ status: string | null;
224
+ }
225
+ interface PlatformBalanceSnapshot {
226
+ balance: number;
227
+ lifetimeSpent: number;
228
+ updatedAt?: string;
229
+ }
230
+ interface PlatformUsageProductRow {
231
+ product: string | null;
232
+ totalSpent: number;
233
+ count: number;
234
+ }
235
+ interface PlatformBillingHttp {
236
+ /** GET /v1/plans/current (user bearer). */
237
+ getSubscription(userApiKey: string): Promise<PlatformSubscriptionInfo>;
238
+ /** GET /v1/billing/balance (user bearer). */
239
+ getBalance(userApiKey: string): Promise<PlatformBalanceSnapshot>;
240
+ /** GET /v1/billing/usage (user bearer). */
241
+ getUsageByProduct(userApiKey: string): Promise<PlatformUsageProductRow[]>;
242
+ /** POST /v1/billing/deduct (service token). */
243
+ deduct(input: {
244
+ platformUserId: string;
245
+ amountUsd: number;
246
+ type: string;
247
+ description: string;
248
+ referenceId: string;
249
+ }): Promise<void>;
250
+ /** Absolute URL of the platform's billing-management surface. */
251
+ billingUrl(): string;
252
+ }
253
+ declare function createPlatformBillingHttp(opts: PlatformBillingHttpOptions): PlatformBillingHttp;
254
+ interface TangleTierPolicy {
255
+ concurrency: number;
256
+ overageAllowed: boolean;
257
+ }
258
+ declare const DEFAULT_TANGLE_TIER_POLICY: Record<TanglePlanTier, TangleTierPolicy>;
259
+ interface TangleTierState {
260
+ tier: TanglePlanTier;
261
+ subscriptionStatus: string | null;
262
+ remainingBalanceUsd: number;
263
+ lifetimeSpentUsd: number;
264
+ concurrency: number;
265
+ overageAllowed: boolean;
266
+ }
267
+ /**
268
+ * Read subscription + balance and project them onto the tier policy. A
269
+ * null/absent key fails CLOSED (free tier, zero balance) — a billable run is
270
+ * never started against an unknown balance. Platform errors throw; callers
271
+ * on the billable path choose their posture explicitly.
272
+ */
273
+ declare function readTangleTierState(http: PlatformBillingHttp, userApiKey: string | null | undefined, policy?: Record<TanglePlanTier, TangleTierPolicy>): Promise<TangleTierState>;
274
+ interface PlatformIdentityStore {
275
+ resolveIdentity(userId: string): Promise<PlatformIdentity | null>;
276
+ }
277
+ /** Concrete fetch-backed `PlatformBillingClient<TanglePlanTier>` for
278
+ * `createPlatformBalanceManager` (from `/billing`). */
279
+ declare function createTanglePlatformBillingClient(http: PlatformBillingHttp, identity: PlatformIdentityStore): PlatformBillingClient<TanglePlanTier>;
280
+
281
+ /**
282
+ * Request guards for agent-app routes: session auth (302 redirect for pages,
283
+ * JSON 401 for APIs), admin allowlisting (404 — the route stays invisible to
284
+ * non-admins), and the billable-balance gate (402 with a stable code).
285
+ * Session resolution is a seam; thrown Responses follow the router convention
286
+ * of surfacing a thrown Response as the route result.
287
+ */
288
+ interface AuthGuardOptions<Session> {
289
+ /** e.g. a better-auth `auth.api.getSession` wrapped by the app. */
290
+ getSession(request: Request): Promise<Session | null | undefined>;
291
+ /** Default '/login'. */
292
+ loginPath?: string;
293
+ }
294
+ interface AuthGuard<Session> {
295
+ /** Page guard — throws a 302 redirect Response to `loginPath`. */
296
+ requireUser(request: Request): Promise<Session>;
297
+ /** API guard — throws JSON 401 `{ error: 'Unauthorized', code: 'auth.unauthenticated' }`. */
298
+ requireApiUser(request: Request): Promise<Session>;
299
+ /** `apiResponse` selects the 401 JSON path over the redirect. */
300
+ requireSession(request: Request, opts?: {
301
+ apiResponse?: boolean;
302
+ }): Promise<Session>;
303
+ getOptionalSession(request: Request): Promise<Session | null>;
304
+ }
305
+ declare function createAuthGuard<Session>(opts: AuthGuardOptions<Session>): AuthGuard<Session>;
306
+ /** Comma/whitespace separated → trimmed, lowercased, empties dropped. */
307
+ declare function parseAdminEmails(raw: string | null | undefined): string[];
308
+ interface AdminGuardOptions<Session> {
309
+ requireUser(request: Request): Promise<Session>;
310
+ emailOf(session: Session): string | null | undefined;
311
+ /** Resolved per request; an EMPTY allowlist refuses everyone. */
312
+ allowedEmails(): string[];
313
+ }
314
+ /** Non-admins (and empty allowlists) get 404, keeping the route invisible —
315
+ * better than a "forbidden" footprint that advertises its existence. */
316
+ declare function createAdminGuard<Session>(opts: AdminGuardOptions<Session>): (request: Request) => Promise<Session>;
317
+ interface BillableBalanceState {
318
+ overageAllowed: boolean;
319
+ remainingBalanceUsd: number;
320
+ }
321
+ interface AssertBillableBalanceOptions {
322
+ env?: Record<string, string | undefined>;
323
+ /** App-specific enforcement override flag (e.g. 'GTM_BILLING_ENFORCEMENT'),
324
+ * fed to `isTangleBillingEnforcementDisabled`. */
325
+ enforcementEnvVar?: string;
326
+ /** Default 'Add balance or upgrade your plan to invoke this agent.'. */
327
+ errorMessage?: string;
328
+ /** Merged into the 402 JSON body (e.g. `{ organizationId }`). */
329
+ errorBody?: Record<string, unknown>;
330
+ }
331
+ /**
332
+ * Gate a billable turn: passes when enforcement is disabled (dev default),
333
+ * the tier allows overage, or remaining balance is positive. Otherwise throws
334
+ * a 402 Response with the stable `billing.balance_required` code so clients
335
+ * can route to the billing screen.
336
+ */
337
+ declare function assertBillableBalance(state: BillableBalanceState, opts?: AssertBillableBalanceOptions): void;
338
+
339
+ export { type AdminGuardOptions, type AssertBillableBalanceOptions, type AuthGuard, type AuthGuardOptions, type BillableBalanceState, DEFAULT_TANGLE_TIER_POLICY, type HubClientLike, type HubProxyContext, type HubProxyRouteArgs, type HubProxyRoutes, type PlatformBalanceSnapshot, type PlatformBillingHttp, PlatformBillingHttpError, type PlatformBillingHttpOptions, type PlatformIdentityStore, type PlatformSubscriptionInfo, type PlatformUsageProductRow, type SsoStateConfig, TangleBearerMissingError, type TanglePlanTier, type TangleSsoAccountStore, type TangleSsoAuthClient, type TangleSsoExchangeResult, type TangleSsoHandlerOptions, type TangleSsoHandlers, TangleSsoUserCreateError, type TangleTierPolicy, type TangleTierState, assertBillableBalance, createAdminGuard, createAuthGuard, createHubProxyRoutes, createPlatformBillingHttp, createSignedSsoState, createTanglePlatformBillingClient, createTangleSsoHandlers, isPlatformBillingHttpError, isPlatformHubErrorLike, isTangleBearerMissingError, normalizeTanglePlanTier, parseAdminEmails, readTangleTierState, verifySignedSsoState };