@tangle-network/agent-app 0.49.3 → 0.49.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/dist/app-auth/index.js +1 -1
- package/dist/{chunk-TWTLQWES.js → chunk-FXWNNHPC.js} +16 -3
- package/dist/{chunk-TWTLQWES.js.map → chunk-FXWNNHPC.js.map} +1 -1
- package/dist/design-canvas-react/index.js +4 -4
- package/dist/hosted-agent/index.d.ts +139 -0
- package/dist/hosted-agent/index.js +254 -0
- package/dist/hosted-agent/index.js.map +1 -0
- package/dist/platform/index.js +1 -1
- package/dist/teams-react/index.js +4 -4
- package/package.json +11 -1
package/dist/app-auth/index.js
CHANGED
|
@@ -157,9 +157,22 @@ function createBetterAuthSessionCookieMinter(auth, options = {}) {
|
|
|
157
157
|
return cookies;
|
|
158
158
|
};
|
|
159
159
|
}
|
|
160
|
+
var REDIRECT_REFERENCE_ORIGIN = "https://redirect-reference.invalid";
|
|
160
161
|
function sanitizeRedirectPath(value, fallback) {
|
|
161
|
-
if (value
|
|
162
|
-
|
|
162
|
+
if (!value || !value.startsWith("/") || /[\\\u0000-\u001f\u007f]/.test(value)) return fallback;
|
|
163
|
+
const path = resolveOnReferenceOrigin(value);
|
|
164
|
+
if (path === null || resolveOnReferenceOrigin(path) !== path) return fallback;
|
|
165
|
+
return path;
|
|
166
|
+
}
|
|
167
|
+
function resolveOnReferenceOrigin(value) {
|
|
168
|
+
let url;
|
|
169
|
+
try {
|
|
170
|
+
url = new URL(value, REDIRECT_REFERENCE_ORIGIN);
|
|
171
|
+
} catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
if (url.origin !== REDIRECT_REFERENCE_ORIGIN) return null;
|
|
175
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
163
176
|
}
|
|
164
177
|
function redirectResponse(location, headers = new Headers()) {
|
|
165
178
|
headers.set("Location", location);
|
|
@@ -374,4 +387,4 @@ export {
|
|
|
374
387
|
createAdminGuard,
|
|
375
388
|
assertBillableBalance
|
|
376
389
|
};
|
|
377
|
-
//# sourceMappingURL=chunk-
|
|
390
|
+
//# sourceMappingURL=chunk-FXWNNHPC.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/platform/sso.ts","../src/platform/guards.ts"],"sourcesContent":["/**\n * Cross-site Tangle SSO for agent apps: signed-state CSRF cookies plus the\n * full start/callback orchestration against the platform's /cross-site\n * bridge. The platform wire client and account persistence are structural\n * seams (`TangleSsoAuthClient` / `TangleSsoAccountStore`), so this module\n * never imports agent-runtime, an auth framework, or a database driver.\n * WebCrypto only — runs in workerd without node compatibility flags.\n */\n\nimport { clearCookieHeader, readCookieValue, serializeCookie } from '../web/index'\n\nconst DEFAULT_STATE_TTL_SECONDS = 600\nconst DEFAULT_SESSION_TTL_SECONDS = 60 * 60 * 24 * 7\nconst DEFAULT_REDIRECT_PATH = '/app'\nconst DEFAULT_LOGIN_PATH = '/login'\nconst DEFAULT_SESSION_COOKIE = 'better-auth.session_token'\n\n// ── Signed state ────────────────────────────────────────────────────────────\n\n/** Define configuration options for managing SSO state including secret, lifetime, and clock injection */\nexport interface SsoStateConfig {\n /** HMAC-SHA256 secret (e.g. the app's auth secret). */\n secret: string\n /** State lifetime in ms. Default 600 000. */\n ttlMs?: number\n /** Injectable clock (ms since epoch). Default Date.now. */\n now?: () => number\n}\n\nfunction randomHex(bytes: number): string {\n const buf = new Uint8Array(bytes)\n crypto.getRandomValues(buf)\n return Array.from(buf, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n\nasync function hmacBytes(secret: string, value: string): Promise<Uint8Array> {\n const key = await crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n )\n return new Uint8Array(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(value)))\n}\n\nasync function hmacHex(secret: string, value: string): Promise<string> {\n return Array.from(await hmacBytes(secret, value), (b) => b.toString(16).padStart(2, '0')).join('')\n}\n\nfunction constantTimeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false\n let diff = 0\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i)\n return diff === 0\n}\n\n/** Mint a `<randomHex32>.<timestamp36>.<hmacHex>` state value. The timestamp\n * is inside the signed payload, so expiry survives cookie-attribute tampering. */\nexport async function createSignedSsoState(config: SsoStateConfig): Promise<string> {\n if (!config.secret) throw new Error('SsoStateConfig.secret is required')\n const now = config.now ?? Date.now\n const payload = `${randomHex(16)}.${now().toString(36)}`\n return `${payload}.${await hmacHex(config.secret, payload)}`\n}\n\n/** Verify the MAC (constant-time) and the signed TTL. */\nexport async function verifySignedSsoState(state: string, config: SsoStateConfig): Promise<boolean> {\n if (!config.secret) throw new Error('SsoStateConfig.secret is required')\n const parts = state.split('.')\n if (parts.length !== 3) return false\n const [random, timestamp, mac] = parts\n if (!random || !timestamp || !mac) return false\n const expected = await hmacHex(config.secret, `${random}.${timestamp}`)\n if (!constantTimeEqual(mac, expected)) return false\n const mintedAt = parseInt(timestamp, 36)\n if (!Number.isFinite(mintedAt)) return false\n const now = config.now ?? Date.now\n const ttlMs = config.ttlMs ?? DEFAULT_STATE_TTL_SECONDS * 1000\n return now() - mintedAt <= ttlMs\n}\n\n// ── Seams ───────────────────────────────────────────────────────────────────\n\n/** Describe the result of exchanging SSO credentials including API key, user info, and optional plan details */\nexport interface TangleSsoExchangeResult {\n apiKey: string\n /** The platform's canonical verified-email proof. */\n emailVerified: boolean\n user: { id: string; email: string; name?: string | null }\n plan?: { tier: string } | null\n}\n\n/** Structural mirror of the platform auth wire client — any object with these\n * two methods satisfies it without this module importing the concrete class. */\nexport interface TangleSsoAuthClient {\n authorizeUrl(options: { state: string; redirectUri?: string }): string\n exchange(code: string): Promise<TangleSsoExchangeResult>\n}\n\n/** Local account shape required by the verified Tangle SSO account policy.\n * Consumers map their user/link tables into this shape before resolving an\n * exchange. */\nexport interface TangleSsoLocalAccount {\n userId: string\n email: string\n emailVerified: boolean\n platformUserId: string | null\n}\n\n/** Reasons a local account lookup must stop instead of linking an exchange. */\nexport type TangleSsoAccountConflictReason =\n | 'ambiguous-platform-id'\n | 'platform-id-email-conflict'\n | 'unverified-email'\n | 'email-platform-id-conflict'\n | 'ambiguous-email'\n | 'invalid-platform-id'\n | 'invalid-email'\n | 'platform-match-mismatch'\n | 'email-match-mismatch'\n\n/** Result of applying the verified Tangle SSO account matching policy. */\nexport type TangleSsoAccountResolution =\n | { kind: 'existing'; userId: string; matchedBy: 'platform-id' | 'verified-email' }\n | { kind: 'create' }\n | { kind: 'reject'; reason: TangleSsoAccountConflictReason }\n\n/** Inputs for {@link resolveTangleSsoAccount}. `platformUserId` is the\n * platform's stable identity. */\nexport interface TangleSsoAccountResolutionInput {\n email: string\n platformMatches: readonly TangleSsoLocalAccount[]\n emailMatches: readonly TangleSsoLocalAccount[]\n platformUserId: string\n}\n\n/** Normalize an email for comparisons and persistence queries. */\nexport function normalizeTangleSsoEmail(email: string): string {\n return email.trim().toLowerCase()\n}\n\nfunction normalizedPlatformUserId(value: string | null | undefined): string | null {\n const normalized = value?.trim()\n return normalized ? normalized : null\n}\n\n/**\n * Resolve a platform exchange against all local rows found by stable platform\n * identity and normalized email.\n *\n * A stable platform id takes precedence over email. Every ambiguous or\n * contradictory result rejects, and email fallback requires a verified local\n * email that is not already bound to another platform id. The resolver is\n * pure so each consumer can query its own schema, including schemas without\n * unique indexes, then rerun it after an insert race.\n */\nexport function resolveTangleSsoAccount(\n input: TangleSsoAccountResolutionInput,\n): TangleSsoAccountResolution {\n const email = normalizeTangleSsoEmail(input.email)\n const platformUserId = normalizedPlatformUserId(input.platformUserId)\n if (!email) return { kind: 'reject', reason: 'invalid-email' }\n if (!platformUserId) return { kind: 'reject', reason: 'invalid-platform-id' }\n\n if (input.platformMatches.length > 1) {\n return { kind: 'reject', reason: 'ambiguous-platform-id' }\n }\n if (input.emailMatches.length > 1) {\n return { kind: 'reject', reason: 'ambiguous-email' }\n }\n\n if (input.platformMatches.some((account) => normalizedPlatformUserId(account.platformUserId) !== platformUserId)) {\n return { kind: 'reject', reason: 'platform-match-mismatch' }\n }\n if (input.emailMatches.some((account) => normalizeTangleSsoEmail(account.email) !== email)) {\n return { kind: 'reject', reason: 'email-match-mismatch' }\n }\n\n const platformMatch = input.platformMatches[0]\n const emailMatch = input.emailMatches[0]\n\n if (platformMatch) {\n if (emailMatch && emailMatch.userId !== platformMatch.userId) {\n return { kind: 'reject', reason: 'platform-id-email-conflict' }\n }\n return { kind: 'existing', userId: platformMatch.userId, matchedBy: 'platform-id' }\n }\n\n if (!emailMatch) return { kind: 'create' }\n const existingPlatformUserId = normalizedPlatformUserId(emailMatch.platformUserId)\n if (existingPlatformUserId === platformUserId) {\n return { kind: 'existing', userId: emailMatch.userId, matchedBy: 'platform-id' }\n }\n if (emailMatch.emailVerified !== true) {\n return { kind: 'reject', reason: 'unverified-email' }\n }\n\n if (existingPlatformUserId && existingPlatformUserId !== platformUserId) {\n return { kind: 'reject', reason: 'email-platform-id-conflict' }\n }\n\n // The platform proof is already verified. A verified local email row is safe\n // to link when it is unbound or already carries this same platform id.\n return { kind: 'existing', userId: emailMatch.userId, matchedBy: 'verified-email' }\n}\n\n/** Thrown by an account store when the verified SSO policy rejects a link. */\nexport class TangleSsoAccountConflictError extends Error {\n readonly reason: TangleSsoAccountConflictReason\n\n constructor(reason: TangleSsoAccountConflictReason, message = 'Tangle SSO account linking was rejected') {\n super(message)\n this.name = 'TangleSsoAccountConflictError'\n this.reason = reason\n }\n}\n\n/** Thrown by `upsertUserByEmail` when the app-local user row cannot be\n * created; the callback handler maps it to `?error=tangle_user_create_failed`.\n * Any other store error propagates. */\nexport class TangleSsoUserCreateError extends Error {\n constructor(message = 'Failed to create local user for Tangle SSO') {\n super(message)\n this.name = 'TangleSsoUserCreateError'\n }\n}\n\n/**\n * Account persistence seam. Covers both storage styles in use: link-table\n * apps (a per-user platform-link row) and session-column apps (the key on the\n * session row) — `saveTangleLink` receives both `userId` and `sessionToken`,\n * and each app persists with the key it needs. `createSession` runs first so\n * the token is always available to `saveTangleLink`.\n */\nexport interface TangleSsoAccountStore {\n /** Resolve the local account before any user, session, or link write.\n * Implementations must query every candidate by stable platform id and\n * normalized email, then apply `resolveTangleSsoAccount` (or an equivalent\n * fail-closed policy). The callback rejects `kind: 'reject'` results before\n * it calls `upsertUserByEmail`. */\n resolveAccount(input: { email: string; platformUserId: string }): Promise<TangleSsoAccountResolution>\n /** Find-or-create the app-local user selected by `resolveAccount`.\n * `tangleUserId` is the platform's stable user id — match on it first when\n * the app stores it (emails are mutable on the platform; the id is not),\n * falling back to email for first-time logins. */\n upsertUserByEmail(input: {\n email: string\n name: string | null\n tangleUserId: string\n resolution: Exclude<TangleSsoAccountResolution, { kind: 'reject' }>\n }): Promise<{ userId: string }>\n /** Create an app session row; returns the session-cookie token value. */\n createSession(input: {\n userId: string\n expiresAt: Date\n ipAddress: string | null\n userAgent: string | null\n }): Promise<{ token: string }>\n /** Persist the platform link (API key + platform identity). */\n saveTangleLink(input: {\n userId: string\n sessionToken: string\n tangleUserId: string\n email: string\n name: string | null\n apiKey: string\n planTier: string | null\n }): Promise<void>\n}\n\n// ── Session cookie ──────────────────────────────────────────────────────────\n\n/** Successful-login context handed to the `setSessionCookie` seam. */\nexport interface TangleSsoSessionCookieArgs {\n /** Session token returned by `store.createSession`. */\n token: string\n /** Session expiry (now + `sessionTtlSeconds`). */\n expiresAt: Date\n /** Mirrors `sessionTtlSeconds` after defaulting. */\n ttlSeconds: number\n /** Mirrors `TangleSsoHandlerOptions.secureCookies`. */\n secure: boolean\n}\n\n/**\n * Sign a session token to better-call's signed-cookie contract — the value\n * better-auth's `getSignedCookie` verifies: `<token>.<signature>` where the\n * signature is the raw HMAC-SHA256 of the token under `secret`, encoded as\n * STANDARD base64 WITH padding (32 bytes → 44 chars ending `=`; better-call\n * rejects any other length or suffix, so url-safe/unpadded variants read back\n * as a null session). The joined value is percent-encoded once at cookie\n * serialization, matching better-call's `serializeSignedCookie` byte-exactly.\n */\nexport async function signSessionCookieValue(token: string, secret: string): Promise<string> {\n if (!secret) throw new Error('signSessionCookieValue requires a non-empty secret')\n const sig = await hmacBytes(secret, token)\n let bin = ''\n for (const byte of sig) bin += String.fromCharCode(byte)\n return `${token}.${btoa(bin)}`\n}\n\n/** Structural slice of a `betterAuth()` instance — only what cookie minting\n * reads. No better-auth import: the signing contract is implemented by\n * `signSessionCookieValue`, byte-compatible with better-auth's own\n * `makeSignature`. */\nexport interface BetterAuthSessionCookieSource {\n $context: PromiseLike<{\n secret: string\n authCookies: {\n sessionToken: {\n /** Final cookie name — better-auth decides the `__Secure-` prefix\n * (and any `advanced.cookiePrefix`) once at `betterAuth()` init. */\n name: string\n attributes: {\n secure?: boolean\n sameSite?: string\n path?: string\n httpOnly?: boolean\n domain?: string\n }\n }\n }\n }>\n}\n\n/** Define options to customize warning behavior for shadowed cookie names in authentication sessions */\nexport interface BetterAuthSessionCookieMinterOptions {\n /** Receives the shadowed-cookie-name warning (see below). Default\n * console.warn. */\n warn?: (message: string) => void\n}\n\n/**\n * Canonical `setSessionCookie` wiring for better-auth apps: mint the session\n * Set-Cookie exactly as better-auth's own login flows do — name + attributes\n * from `auth.$context.authCookies.sessionToken` (better-auth stays\n * authoritative over prefix/name/attributes) and the value signed to\n * better-call's `getSignedCookie` contract. A raw unprefixed\n * `better-auth.session_token` left by an earlier login is explicitly expired\n * so it cannot shadow the real cookie.\n *\n * Warns when the app's session cookie still has better-auth's DEFAULT name:\n * the Tangle platform (id.tangle.tools) sets a `Domain=.tangle.tools` cookie\n * under that exact name, and equal-path cookies are sent oldest-first — the\n * platform's cookie is always older (the user signs in there before the app's\n * callback runs), so the app reads the platform's token, fails its own\n * signature check, and every fresh login lands logged-out. Per-app\n * `advanced.cookiePrefix` is the fix.\n *\n * Throws on a domain-scoped session cookie for the same reason: a\n * `Domain=`-wide session cookie is exactly the shadowing footgun.\n */\nexport function createBetterAuthSessionCookieMinter(\n auth: BetterAuthSessionCookieSource,\n options: BetterAuthSessionCookieMinterOptions = {},\n): (args: TangleSsoSessionCookieArgs) => Promise<string[]> {\n const warn = options.warn ?? ((message: string) => console.warn(message))\n return async ({ token, ttlSeconds }) => {\n const ctx = await auth.$context\n if (!ctx.secret) {\n throw new Error('createBetterAuthSessionCookieMinter: auth context has no secret')\n }\n const { name, attributes } = ctx.authCookies.sessionToken\n if (attributes.domain) {\n throw new Error(\n `createBetterAuthSessionCookieMinter: refusing a domain-scoped session cookie (Domain=${attributes.domain}) — ` +\n 'a domain-wide session cookie shadows sibling apps that share the parent domain',\n )\n }\n if (name === DEFAULT_SESSION_COOKIE || name === `__Secure-${DEFAULT_SESSION_COOKIE}`) {\n warn(\n `[tangle-sso] session cookie is named \"${name}\" — better-auth's default. ` +\n 'The Tangle platform (id.tangle.tools) sets a Domain=.tangle.tools cookie under the same name, ' +\n \"and the platform's (older) cookie wins the Cookie-header order, so this app's sessions read back null. \" +\n \"Set a per-app prefix: betterAuth({ advanced: { cookiePrefix: '<app>' } }).\",\n )\n }\n const sameSite = typeof attributes.sameSite === 'string' ? attributes.sameSite : 'lax'\n const cookieOptions = {\n name,\n path: typeof attributes.path === 'string' ? attributes.path : '/',\n httpOnly: attributes.httpOnly !== false,\n sameSite: (sameSite.charAt(0).toUpperCase() + sameSite.slice(1)) as 'Lax' | 'Strict' | 'None',\n secure: Boolean(attributes.secure) || name.startsWith('__Secure-'),\n maxAgeSeconds: ttlSeconds,\n }\n const cookies = [serializeCookie(await signSessionCookieValue(token, ctx.secret), cookieOptions)]\n if (name !== DEFAULT_SESSION_COOKIE) {\n cookies.push(clearCookieHeader({ ...cookieOptions, name: DEFAULT_SESSION_COOKIE }))\n }\n return cookies\n }\n}\n\n// ── Handlers ────────────────────────────────────────────────────────────────\n\n/** Define configuration options for handling Tangle SSO authentication and session management */\nexport interface TangleSsoHandlerOptions {\n auth: TangleSsoAuthClient\n store: TangleSsoAccountStore\n /** HMAC secret for the state cookie. */\n stateSecret: string\n /** Absolute callback URL registered with the platform. */\n callbackUrl: string\n stateCookieName: string\n /** Default 'better-auth.session_token'. Ignored when `setSessionCookie` is\n * provided. The default path prepends `__Secure-` iff `secureCookies`. */\n sessionCookieName?: string\n /** Mint the host auth framework's own session cookie(s); return complete\n * Set-Cookie header values (the handler appends them verbatim and sets no\n * session cookie itself). Supply this when the framework should stay\n * authoritative over name/prefix/signing/attributes — e.g. better-auth:\n * `auth.$context.authCookies.sessionToken` + `makeSignature`. */\n setSessionCookie?: (\n args: TangleSsoSessionCookieArgs,\n ) => readonly string[] | Promise<readonly string[]>\n /** HMAC-SHA256 secret the host auth framework verifies session cookies with\n * (better-auth: its `secret`). Required when `setSessionCookie` is absent —\n * the default cookie is minted to better-call's signed contract via\n * `signSessionCookieValue`; an unsigned or mis-signed value reads back as a\n * null session, so there is deliberately no fallback to `stateSecret`\n * (which is not guaranteed to be the auth secret). */\n sessionCookieSecret?: string\n /** Adds `Secure` to every cookie this module sets, and (default session\n * cookie only) the `__Secure-` name prefix. Must match the auth\n * framework's own secure-cookie decision (better-auth: https `baseURL` /\n * `advanced.useSecureCookies`), or it will look up a different cookie name\n * than the one set here. */\n secureCookies: boolean\n /** Default 604 800 (7 days). */\n sessionTtlSeconds?: number\n /** Default 600. Applies to both the cookie Max-Age and the signed TTL. */\n stateTtlSeconds?: number\n /** Default '/app'. */\n defaultRedirectPath?: string\n /** Default '/login'. */\n loginPath?: string\n /** Failure log hook (e.g. console.error). Default no-op. */\n log?: (message: string, error?: unknown) => void\n now?: () => number\n}\n\n/** Define handlers for SSO start and callback routes managing authentication flow and session cookies */\nexport interface TangleSsoHandlers {\n /** GET start route: mint + sign state, set the state cookie, 302 to the\n * platform authorize URL. `?redirect=` carries the post-login path. */\n start(request: Request): Promise<Response>\n /** GET callback route: verify state, exchange the code, upsert the user,\n * create the session, save the platform link, set the session cookie\n * (via the `setSessionCookie` seam, else signed to better-call's contract\n * with `sessionCookieSecret`), 302 to the saved redirect. Every failure\n * 302s to `loginPath?error=…` with the state cookie cleared. */\n callback(request: Request): Promise<Response>\n}\n\n/** Accept only same-origin absolute paths (rejects `//host` protocol-relative URLs). */\nfunction sanitizeRedirectPath(value: string | null, fallback: string): string {\n if (value && value.startsWith('/') && !value.startsWith('//')) return value\n return fallback\n}\n\nfunction redirectResponse(location: string, headers = new Headers()): Response {\n headers.set('Location', location)\n return new Response(null, { status: 302, headers })\n}\n\n/** Real client IP: `CF-Connecting-IP` behind Cloudflare, else the first\n * `x-forwarded-for` hop (the rest of the list is sender-controlled). */\nfunction clientIp(request: Request): string | null {\n return (\n request.headers.get('CF-Connecting-IP') ??\n request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ??\n null\n )\n}\n\ninterface StateCookiePayload {\n s: string\n r: string\n}\n\nfunction parseStateCookiePayload(raw: string | null): StateCookiePayload | null {\n if (!raw) return null\n try {\n const parsed = JSON.parse(raw) as unknown\n if (parsed === null || typeof parsed !== 'object') return null\n const { s, r } = parsed as Record<string, unknown>\n if (typeof s !== 'string' || typeof r !== 'string') return null\n return { s, r }\n } catch {\n return null\n }\n}\n\n/** Create Tangle SSO handlers to manage authentication state, callbacks, and session cookies */\nexport function createTangleSsoHandlers(opts: TangleSsoHandlerOptions): TangleSsoHandlers {\n if (!opts.stateSecret) throw new Error('TangleSsoHandlerOptions.stateSecret is required')\n if (!opts.callbackUrl) throw new Error('TangleSsoHandlerOptions.callbackUrl is required')\n if (!opts.stateCookieName) throw new Error('TangleSsoHandlerOptions.stateCookieName is required')\n\n const sessionCookieName = opts.sessionCookieName ?? DEFAULT_SESSION_COOKIE\n\n let mintSessionCookies: (args: TangleSsoSessionCookieArgs) => Promise<readonly string[]>\n if (opts.setSessionCookie) {\n const seam = opts.setSessionCookie\n mintSessionCookies = async (args) => await seam(args)\n } else if (opts.sessionCookieSecret) {\n const secret = opts.sessionCookieSecret\n mintSessionCookies = async ({ token, secure, ttlSeconds }) => [\n serializeCookie(await signSessionCookieValue(token, secret), {\n name: secure ? `__Secure-${sessionCookieName}` : sessionCookieName,\n secure,\n maxAgeSeconds: ttlSeconds,\n }),\n ]\n } else {\n throw new Error(\n 'TangleSsoHandlerOptions requires setSessionCookie or sessionCookieSecret: ' +\n 'better-auth only accepts HMAC-signed (and, on https, __Secure--prefixed) session cookies, ' +\n 'so an unsigned default would mint sessions that read back null',\n )\n }\n const sessionTtlSeconds = opts.sessionTtlSeconds ?? DEFAULT_SESSION_TTL_SECONDS\n const stateTtlSeconds = opts.stateTtlSeconds ?? DEFAULT_STATE_TTL_SECONDS\n const defaultRedirectPath = opts.defaultRedirectPath ?? DEFAULT_REDIRECT_PATH\n const loginPath = opts.loginPath ?? DEFAULT_LOGIN_PATH\n const log = opts.log ?? (() => {})\n const now = opts.now ?? Date.now\n const stateConfig: SsoStateConfig = { secret: opts.stateSecret, ttlMs: stateTtlSeconds * 1000, now }\n\n const stateCookieOpts = { name: opts.stateCookieName, secure: opts.secureCookies }\n\n function loginErrorRedirect(code: string): Response {\n const headers = new Headers()\n headers.append('Set-Cookie', clearCookieHeader(stateCookieOpts))\n return redirectResponse(`${loginPath}?error=${code}`, headers)\n }\n\n return {\n async start(request) {\n const url = new URL(request.url)\n const redirectPath = sanitizeRedirectPath(url.searchParams.get('redirect'), defaultRedirectPath)\n const state = await createSignedSsoState(stateConfig)\n const cookie = serializeCookie(JSON.stringify({ s: state, r: redirectPath }), {\n ...stateCookieOpts,\n maxAgeSeconds: stateTtlSeconds,\n })\n const headers = new Headers()\n headers.append('Set-Cookie', cookie)\n return redirectResponse(opts.auth.authorizeUrl({ state, redirectUri: opts.callbackUrl }), headers)\n },\n\n async callback(request) {\n const url = new URL(request.url)\n const code = url.searchParams.get('code')\n const stateFromPlatform = url.searchParams.get('state')\n if (!code || !stateFromPlatform) return loginErrorRedirect('tangle_callback_missing')\n\n const payload = parseStateCookiePayload(readCookieValue(request.headers.get('cookie'), opts.stateCookieName))\n if (!payload || payload.s !== stateFromPlatform) return loginErrorRedirect('tangle_state_mismatch')\n if (!(await verifySignedSsoState(payload.s, stateConfig))) return loginErrorRedirect('tangle_state_mismatch')\n\n let exchanged: TangleSsoExchangeResult\n try {\n exchanged = await opts.auth.exchange(code)\n } catch (err) {\n log('[tangle-sso] exchange failed', err)\n return loginErrorRedirect('tangle_exchange_failed')\n }\n if (exchanged.emailVerified !== true) {\n log('[tangle-sso] exchange did not include a verified-email proof')\n return loginErrorRedirect('tangle_exchange_failed')\n }\n\n let userId: string\n try {\n const email = normalizeTangleSsoEmail(exchanged.user.email)\n const resolution = await opts.store.resolveAccount({\n email,\n platformUserId: exchanged.user.id,\n })\n if (resolution.kind === 'reject') {\n throw new TangleSsoAccountConflictError(resolution.reason)\n }\n ;({ userId } = await opts.store.upsertUserByEmail({\n email,\n name: exchanged.user.name ?? null,\n tangleUserId: exchanged.user.id,\n resolution,\n }))\n } catch (err) {\n if (err instanceof TangleSsoAccountConflictError) {\n log('[tangle-sso] account linking conflict', err)\n return loginErrorRedirect('tangle_account_conflict')\n }\n if (err instanceof TangleSsoUserCreateError) return loginErrorRedirect('tangle_user_create_failed')\n throw err\n }\n\n const expiresAt = new Date(now() + sessionTtlSeconds * 1000)\n const { token } = await opts.store.createSession({\n userId,\n expiresAt,\n ipAddress: clientIp(request),\n userAgent: request.headers.get('user-agent'),\n })\n\n await opts.store.saveTangleLink({\n userId,\n sessionToken: token,\n tangleUserId: exchanged.user.id,\n email: normalizeTangleSsoEmail(exchanged.user.email),\n name: exchanged.user.name ?? null,\n apiKey: exchanged.apiKey,\n planTier: exchanged.plan?.tier ?? null,\n })\n\n const headers = new Headers()\n headers.append('Set-Cookie', clearCookieHeader(stateCookieOpts))\n const sessionCookies = await mintSessionCookies({\n token,\n expiresAt,\n ttlSeconds: sessionTtlSeconds,\n secure: opts.secureCookies,\n })\n for (const cookie of sessionCookies) headers.append('Set-Cookie', cookie)\n return redirectResponse(sanitizeRedirectPath(payload.r, defaultRedirectPath), headers)\n },\n }\n}\n","/**\n * Request guards for agent-app routes: session auth (302 redirect for pages,\n * JSON 401 for APIs), admin allowlisting (404 — the route stays invisible to\n * non-admins), and the billable-balance gate (402 with a stable code).\n * Session resolution is a seam; thrown Responses follow the router convention\n * of surfacing a thrown Response as the route result.\n */\n\nimport { isTangleBillingEnforcementDisabled } from '../runtime/model'\n\n/** Define options for configuring authentication guard behavior and session retrieval */\nexport interface AuthGuardOptions<Session> {\n /** e.g. a better-auth `auth.api.getSession` wrapped by the app. */\n getSession(request: Request): Promise<Session | null | undefined>\n /** Default '/login'. */\n loginPath?: string\n}\n\n/** Resolve user authentication and session requirements for page and API requests */\nexport interface AuthGuard<Session> {\n /** Page guard — throws a 302 redirect Response to `loginPath`. */\n requireUser(request: Request): Promise<Session>\n /** API guard — throws JSON 401 `{ error: 'Unauthorized', code: 'auth.unauthenticated' }`. */\n requireApiUser(request: Request): Promise<Session>\n /** `apiResponse` selects the 401 JSON path over the redirect. */\n requireSession(request: Request, opts?: { apiResponse?: boolean }): Promise<Session>\n getOptionalSession(request: Request): Promise<Session | null>\n}\n\n/** Create an authentication guard that enforces session presence and handles unauthorized access responses */\nexport function createAuthGuard<Session>(opts: AuthGuardOptions<Session>): AuthGuard<Session> {\n const loginPath = opts.loginPath ?? '/login'\n\n async function requireSession(request: Request, o: { apiResponse?: boolean } = {}): Promise<Session> {\n const session = await opts.getSession(request)\n if (!session) {\n if (o.apiResponse) {\n throw Response.json({ error: 'Unauthorized', code: 'auth.unauthenticated' }, { status: 401 })\n }\n throw new Response(null, { status: 302, headers: { Location: loginPath } })\n }\n return session\n }\n\n return {\n requireSession,\n requireUser: (request) => requireSession(request),\n requireApiUser: (request) => requireSession(request, { apiResponse: true }),\n getOptionalSession: async (request) => (await opts.getSession(request)) ?? null,\n }\n}\n\n/** Resolve a value or an HTTP response indicating failure in a guarded operation */\nexport type GuardResolution<T> = { ok: true; value: T } | { ok: false; response: Response }\n\n/**\n * Adapt a guard that THROWS a Response (the quartet above — the router\n * convention) to the `{ok: true, value} | {ok: false, response}` resolution\n * shape the route factories take (`/chat-routes` `authorize`,\n * `/interactions` `resolveConnection`, `/chat-routes` upload `authorize`).\n * Every product wrote this try/catch by hand; it lives here once.\n */\nexport async function guardResolution<T>(run: () => Promise<T>): Promise<GuardResolution<T>> {\n try {\n return { ok: true, value: await run() }\n } catch (err) {\n if (err instanceof Response) return { ok: false, response: err }\n throw err\n }\n}\n\n/** Comma/whitespace separated → trimmed, lowercased, empties dropped. */\nexport function parseAdminEmails(raw: string | null | undefined): string[] {\n return (raw ?? '')\n .split(/[,\\s]+/)\n .map((e) => e.trim().toLowerCase())\n .filter(Boolean)\n}\n\n/** Define options to resolve user session and control access based on allowed admin emails */\nexport interface AdminGuardOptions<Session> {\n requireUser(request: Request): Promise<Session>\n emailOf(session: Session): string | null | undefined\n /** Resolved per request; an EMPTY allowlist refuses everyone. */\n allowedEmails(): string[]\n}\n\n/** Non-admins (and empty allowlists) get 404, keeping the route invisible —\n * better than a \"forbidden\" footprint that advertises its existence. */\nexport function createAdminGuard<Session>(opts: AdminGuardOptions<Session>): (request: Request) => Promise<Session> {\n return async (request) => {\n const session = await opts.requireUser(request)\n const allowed = opts.allowedEmails()\n if (allowed.length === 0) throw new Response('Not found', { status: 404 })\n const email = (opts.emailOf(session) ?? '').toLowerCase()\n if (!allowed.includes(email)) throw new Response('Not found', { status: 404 })\n return session\n }\n}\n\n/** Describe the billable balance state including overage permission and remaining USD balance */\nexport interface BillableBalanceState {\n overageAllowed: boolean\n remainingBalanceUsd: number\n}\n\n/** Define options to assert and customize billable balance enforcement behavior */\nexport interface AssertBillableBalanceOptions {\n env?: Record<string, string | undefined>\n /** App-specific enforcement override flag (e.g. 'GTM_BILLING_ENFORCEMENT'),\n * fed to `isTangleBillingEnforcementDisabled`. */\n enforcementEnvVar?: string\n /** Default 'Add balance or upgrade your plan to invoke this agent.'. */\n errorMessage?: string\n /** Merged into the 402 JSON body (e.g. `{ organizationId }`). */\n errorBody?: Record<string, unknown>\n}\n\n/**\n * Gate a billable turn: passes when enforcement is disabled (dev default),\n * the tier allows overage, or remaining balance is positive. Otherwise throws\n * a 402 Response with the stable `billing.balance_required` code so clients\n * can route to the billing screen.\n */\nexport function assertBillableBalance(state: BillableBalanceState, opts: AssertBillableBalanceOptions = {}): void {\n if (isTangleBillingEnforcementDisabled({ env: opts.env, enforcementEnvVar: opts.enforcementEnvVar })) return\n if (state.overageAllowed || state.remainingBalanceUsd > 0) return\n // errorBody first: the stable error/code contract always wins over caller extras.\n throw Response.json(\n {\n ...opts.errorBody,\n error: opts.errorMessage ?? 'Add balance or upgrade your plan to invoke this agent.',\n code: 'billing.balance_required',\n },\n { status: 402 },\n )\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/platform/sso.ts","../src/platform/guards.ts"],"sourcesContent":["/**\n * Cross-site Tangle SSO for agent apps: signed-state CSRF cookies plus the\n * full start/callback orchestration against the platform's /cross-site\n * bridge. The platform wire client and account persistence are structural\n * seams (`TangleSsoAuthClient` / `TangleSsoAccountStore`), so this module\n * never imports agent-runtime, an auth framework, or a database driver.\n * WebCrypto only — runs in workerd without node compatibility flags.\n */\n\nimport { clearCookieHeader, readCookieValue, serializeCookie } from '../web/index'\n\nconst DEFAULT_STATE_TTL_SECONDS = 600\nconst DEFAULT_SESSION_TTL_SECONDS = 60 * 60 * 24 * 7\nconst DEFAULT_REDIRECT_PATH = '/app'\nconst DEFAULT_LOGIN_PATH = '/login'\nconst DEFAULT_SESSION_COOKIE = 'better-auth.session_token'\n\n// ── Signed state ────────────────────────────────────────────────────────────\n\n/** Define configuration options for managing SSO state including secret, lifetime, and clock injection */\nexport interface SsoStateConfig {\n /** HMAC-SHA256 secret (e.g. the app's auth secret). */\n secret: string\n /** State lifetime in ms. Default 600 000. */\n ttlMs?: number\n /** Injectable clock (ms since epoch). Default Date.now. */\n now?: () => number\n}\n\nfunction randomHex(bytes: number): string {\n const buf = new Uint8Array(bytes)\n crypto.getRandomValues(buf)\n return Array.from(buf, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n\nasync function hmacBytes(secret: string, value: string): Promise<Uint8Array> {\n const key = await crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n )\n return new Uint8Array(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(value)))\n}\n\nasync function hmacHex(secret: string, value: string): Promise<string> {\n return Array.from(await hmacBytes(secret, value), (b) => b.toString(16).padStart(2, '0')).join('')\n}\n\nfunction constantTimeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false\n let diff = 0\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i)\n return diff === 0\n}\n\n/** Mint a `<randomHex32>.<timestamp36>.<hmacHex>` state value. The timestamp\n * is inside the signed payload, so expiry survives cookie-attribute tampering. */\nexport async function createSignedSsoState(config: SsoStateConfig): Promise<string> {\n if (!config.secret) throw new Error('SsoStateConfig.secret is required')\n const now = config.now ?? Date.now\n const payload = `${randomHex(16)}.${now().toString(36)}`\n return `${payload}.${await hmacHex(config.secret, payload)}`\n}\n\n/** Verify the MAC (constant-time) and the signed TTL. */\nexport async function verifySignedSsoState(state: string, config: SsoStateConfig): Promise<boolean> {\n if (!config.secret) throw new Error('SsoStateConfig.secret is required')\n const parts = state.split('.')\n if (parts.length !== 3) return false\n const [random, timestamp, mac] = parts\n if (!random || !timestamp || !mac) return false\n const expected = await hmacHex(config.secret, `${random}.${timestamp}`)\n if (!constantTimeEqual(mac, expected)) return false\n const mintedAt = parseInt(timestamp, 36)\n if (!Number.isFinite(mintedAt)) return false\n const now = config.now ?? Date.now\n const ttlMs = config.ttlMs ?? DEFAULT_STATE_TTL_SECONDS * 1000\n return now() - mintedAt <= ttlMs\n}\n\n// ── Seams ───────────────────────────────────────────────────────────────────\n\n/** Describe the result of exchanging SSO credentials including API key, user info, and optional plan details */\nexport interface TangleSsoExchangeResult {\n apiKey: string\n /** The platform's canonical verified-email proof. */\n emailVerified: boolean\n user: { id: string; email: string; name?: string | null }\n plan?: { tier: string } | null\n}\n\n/** Structural mirror of the platform auth wire client — any object with these\n * two methods satisfies it without this module importing the concrete class. */\nexport interface TangleSsoAuthClient {\n authorizeUrl(options: { state: string; redirectUri?: string }): string\n exchange(code: string): Promise<TangleSsoExchangeResult>\n}\n\n/** Local account shape required by the verified Tangle SSO account policy.\n * Consumers map their user/link tables into this shape before resolving an\n * exchange. */\nexport interface TangleSsoLocalAccount {\n userId: string\n email: string\n emailVerified: boolean\n platformUserId: string | null\n}\n\n/** Reasons a local account lookup must stop instead of linking an exchange. */\nexport type TangleSsoAccountConflictReason =\n | 'ambiguous-platform-id'\n | 'platform-id-email-conflict'\n | 'unverified-email'\n | 'email-platform-id-conflict'\n | 'ambiguous-email'\n | 'invalid-platform-id'\n | 'invalid-email'\n | 'platform-match-mismatch'\n | 'email-match-mismatch'\n\n/** Result of applying the verified Tangle SSO account matching policy. */\nexport type TangleSsoAccountResolution =\n | { kind: 'existing'; userId: string; matchedBy: 'platform-id' | 'verified-email' }\n | { kind: 'create' }\n | { kind: 'reject'; reason: TangleSsoAccountConflictReason }\n\n/** Inputs for {@link resolveTangleSsoAccount}. `platformUserId` is the\n * platform's stable identity. */\nexport interface TangleSsoAccountResolutionInput {\n email: string\n platformMatches: readonly TangleSsoLocalAccount[]\n emailMatches: readonly TangleSsoLocalAccount[]\n platformUserId: string\n}\n\n/** Normalize an email for comparisons and persistence queries. */\nexport function normalizeTangleSsoEmail(email: string): string {\n return email.trim().toLowerCase()\n}\n\nfunction normalizedPlatformUserId(value: string | null | undefined): string | null {\n const normalized = value?.trim()\n return normalized ? normalized : null\n}\n\n/**\n * Resolve a platform exchange against all local rows found by stable platform\n * identity and normalized email.\n *\n * A stable platform id takes precedence over email. Every ambiguous or\n * contradictory result rejects, and email fallback requires a verified local\n * email that is not already bound to another platform id. The resolver is\n * pure so each consumer can query its own schema, including schemas without\n * unique indexes, then rerun it after an insert race.\n */\nexport function resolveTangleSsoAccount(\n input: TangleSsoAccountResolutionInput,\n): TangleSsoAccountResolution {\n const email = normalizeTangleSsoEmail(input.email)\n const platformUserId = normalizedPlatformUserId(input.platformUserId)\n if (!email) return { kind: 'reject', reason: 'invalid-email' }\n if (!platformUserId) return { kind: 'reject', reason: 'invalid-platform-id' }\n\n if (input.platformMatches.length > 1) {\n return { kind: 'reject', reason: 'ambiguous-platform-id' }\n }\n if (input.emailMatches.length > 1) {\n return { kind: 'reject', reason: 'ambiguous-email' }\n }\n\n if (input.platformMatches.some((account) => normalizedPlatformUserId(account.platformUserId) !== platformUserId)) {\n return { kind: 'reject', reason: 'platform-match-mismatch' }\n }\n if (input.emailMatches.some((account) => normalizeTangleSsoEmail(account.email) !== email)) {\n return { kind: 'reject', reason: 'email-match-mismatch' }\n }\n\n const platformMatch = input.platformMatches[0]\n const emailMatch = input.emailMatches[0]\n\n if (platformMatch) {\n if (emailMatch && emailMatch.userId !== platformMatch.userId) {\n return { kind: 'reject', reason: 'platform-id-email-conflict' }\n }\n return { kind: 'existing', userId: platformMatch.userId, matchedBy: 'platform-id' }\n }\n\n if (!emailMatch) return { kind: 'create' }\n const existingPlatformUserId = normalizedPlatformUserId(emailMatch.platformUserId)\n if (existingPlatformUserId === platformUserId) {\n return { kind: 'existing', userId: emailMatch.userId, matchedBy: 'platform-id' }\n }\n if (emailMatch.emailVerified !== true) {\n return { kind: 'reject', reason: 'unverified-email' }\n }\n\n if (existingPlatformUserId && existingPlatformUserId !== platformUserId) {\n return { kind: 'reject', reason: 'email-platform-id-conflict' }\n }\n\n // The platform proof is already verified. A verified local email row is safe\n // to link when it is unbound or already carries this same platform id.\n return { kind: 'existing', userId: emailMatch.userId, matchedBy: 'verified-email' }\n}\n\n/** Thrown by an account store when the verified SSO policy rejects a link. */\nexport class TangleSsoAccountConflictError extends Error {\n readonly reason: TangleSsoAccountConflictReason\n\n constructor(reason: TangleSsoAccountConflictReason, message = 'Tangle SSO account linking was rejected') {\n super(message)\n this.name = 'TangleSsoAccountConflictError'\n this.reason = reason\n }\n}\n\n/** Thrown by `upsertUserByEmail` when the app-local user row cannot be\n * created; the callback handler maps it to `?error=tangle_user_create_failed`.\n * Any other store error propagates. */\nexport class TangleSsoUserCreateError extends Error {\n constructor(message = 'Failed to create local user for Tangle SSO') {\n super(message)\n this.name = 'TangleSsoUserCreateError'\n }\n}\n\n/**\n * Account persistence seam. Covers both storage styles in use: link-table\n * apps (a per-user platform-link row) and session-column apps (the key on the\n * session row) — `saveTangleLink` receives both `userId` and `sessionToken`,\n * and each app persists with the key it needs. `createSession` runs first so\n * the token is always available to `saveTangleLink`.\n */\nexport interface TangleSsoAccountStore {\n /** Resolve the local account before any user, session, or link write.\n * Implementations must query every candidate by stable platform id and\n * normalized email, then apply `resolveTangleSsoAccount` (or an equivalent\n * fail-closed policy). The callback rejects `kind: 'reject'` results before\n * it calls `upsertUserByEmail`. */\n resolveAccount(input: { email: string; platformUserId: string }): Promise<TangleSsoAccountResolution>\n /** Find-or-create the app-local user selected by `resolveAccount`.\n * `tangleUserId` is the platform's stable user id — match on it first when\n * the app stores it (emails are mutable on the platform; the id is not),\n * falling back to email for first-time logins. */\n upsertUserByEmail(input: {\n email: string\n name: string | null\n tangleUserId: string\n resolution: Exclude<TangleSsoAccountResolution, { kind: 'reject' }>\n }): Promise<{ userId: string }>\n /** Create an app session row; returns the session-cookie token value. */\n createSession(input: {\n userId: string\n expiresAt: Date\n ipAddress: string | null\n userAgent: string | null\n }): Promise<{ token: string }>\n /** Persist the platform link (API key + platform identity). */\n saveTangleLink(input: {\n userId: string\n sessionToken: string\n tangleUserId: string\n email: string\n name: string | null\n apiKey: string\n planTier: string | null\n }): Promise<void>\n}\n\n// ── Session cookie ──────────────────────────────────────────────────────────\n\n/** Successful-login context handed to the `setSessionCookie` seam. */\nexport interface TangleSsoSessionCookieArgs {\n /** Session token returned by `store.createSession`. */\n token: string\n /** Session expiry (now + `sessionTtlSeconds`). */\n expiresAt: Date\n /** Mirrors `sessionTtlSeconds` after defaulting. */\n ttlSeconds: number\n /** Mirrors `TangleSsoHandlerOptions.secureCookies`. */\n secure: boolean\n}\n\n/**\n * Sign a session token to better-call's signed-cookie contract — the value\n * better-auth's `getSignedCookie` verifies: `<token>.<signature>` where the\n * signature is the raw HMAC-SHA256 of the token under `secret`, encoded as\n * STANDARD base64 WITH padding (32 bytes → 44 chars ending `=`; better-call\n * rejects any other length or suffix, so url-safe/unpadded variants read back\n * as a null session). The joined value is percent-encoded once at cookie\n * serialization, matching better-call's `serializeSignedCookie` byte-exactly.\n */\nexport async function signSessionCookieValue(token: string, secret: string): Promise<string> {\n if (!secret) throw new Error('signSessionCookieValue requires a non-empty secret')\n const sig = await hmacBytes(secret, token)\n let bin = ''\n for (const byte of sig) bin += String.fromCharCode(byte)\n return `${token}.${btoa(bin)}`\n}\n\n/** Structural slice of a `betterAuth()` instance — only what cookie minting\n * reads. No better-auth import: the signing contract is implemented by\n * `signSessionCookieValue`, byte-compatible with better-auth's own\n * `makeSignature`. */\nexport interface BetterAuthSessionCookieSource {\n $context: PromiseLike<{\n secret: string\n authCookies: {\n sessionToken: {\n /** Final cookie name — better-auth decides the `__Secure-` prefix\n * (and any `advanced.cookiePrefix`) once at `betterAuth()` init. */\n name: string\n attributes: {\n secure?: boolean\n sameSite?: string\n path?: string\n httpOnly?: boolean\n domain?: string\n }\n }\n }\n }>\n}\n\n/** Define options to customize warning behavior for shadowed cookie names in authentication sessions */\nexport interface BetterAuthSessionCookieMinterOptions {\n /** Receives the shadowed-cookie-name warning (see below). Default\n * console.warn. */\n warn?: (message: string) => void\n}\n\n/**\n * Canonical `setSessionCookie` wiring for better-auth apps: mint the session\n * Set-Cookie exactly as better-auth's own login flows do — name + attributes\n * from `auth.$context.authCookies.sessionToken` (better-auth stays\n * authoritative over prefix/name/attributes) and the value signed to\n * better-call's `getSignedCookie` contract. A raw unprefixed\n * `better-auth.session_token` left by an earlier login is explicitly expired\n * so it cannot shadow the real cookie.\n *\n * Warns when the app's session cookie still has better-auth's DEFAULT name:\n * the Tangle platform (id.tangle.tools) sets a `Domain=.tangle.tools` cookie\n * under that exact name, and equal-path cookies are sent oldest-first — the\n * platform's cookie is always older (the user signs in there before the app's\n * callback runs), so the app reads the platform's token, fails its own\n * signature check, and every fresh login lands logged-out. Per-app\n * `advanced.cookiePrefix` is the fix.\n *\n * Throws on a domain-scoped session cookie for the same reason: a\n * `Domain=`-wide session cookie is exactly the shadowing footgun.\n */\nexport function createBetterAuthSessionCookieMinter(\n auth: BetterAuthSessionCookieSource,\n options: BetterAuthSessionCookieMinterOptions = {},\n): (args: TangleSsoSessionCookieArgs) => Promise<string[]> {\n const warn = options.warn ?? ((message: string) => console.warn(message))\n return async ({ token, ttlSeconds }) => {\n const ctx = await auth.$context\n if (!ctx.secret) {\n throw new Error('createBetterAuthSessionCookieMinter: auth context has no secret')\n }\n const { name, attributes } = ctx.authCookies.sessionToken\n if (attributes.domain) {\n throw new Error(\n `createBetterAuthSessionCookieMinter: refusing a domain-scoped session cookie (Domain=${attributes.domain}) — ` +\n 'a domain-wide session cookie shadows sibling apps that share the parent domain',\n )\n }\n if (name === DEFAULT_SESSION_COOKIE || name === `__Secure-${DEFAULT_SESSION_COOKIE}`) {\n warn(\n `[tangle-sso] session cookie is named \"${name}\" — better-auth's default. ` +\n 'The Tangle platform (id.tangle.tools) sets a Domain=.tangle.tools cookie under the same name, ' +\n \"and the platform's (older) cookie wins the Cookie-header order, so this app's sessions read back null. \" +\n \"Set a per-app prefix: betterAuth({ advanced: { cookiePrefix: '<app>' } }).\",\n )\n }\n const sameSite = typeof attributes.sameSite === 'string' ? attributes.sameSite : 'lax'\n const cookieOptions = {\n name,\n path: typeof attributes.path === 'string' ? attributes.path : '/',\n httpOnly: attributes.httpOnly !== false,\n sameSite: (sameSite.charAt(0).toUpperCase() + sameSite.slice(1)) as 'Lax' | 'Strict' | 'None',\n secure: Boolean(attributes.secure) || name.startsWith('__Secure-'),\n maxAgeSeconds: ttlSeconds,\n }\n const cookies = [serializeCookie(await signSessionCookieValue(token, ctx.secret), cookieOptions)]\n if (name !== DEFAULT_SESSION_COOKIE) {\n cookies.push(clearCookieHeader({ ...cookieOptions, name: DEFAULT_SESSION_COOKIE }))\n }\n return cookies\n }\n}\n\n// ── Handlers ────────────────────────────────────────────────────────────────\n\n/** Define configuration options for handling Tangle SSO authentication and session management */\nexport interface TangleSsoHandlerOptions {\n auth: TangleSsoAuthClient\n store: TangleSsoAccountStore\n /** HMAC secret for the state cookie. */\n stateSecret: string\n /** Absolute callback URL registered with the platform. */\n callbackUrl: string\n stateCookieName: string\n /** Default 'better-auth.session_token'. Ignored when `setSessionCookie` is\n * provided. The default path prepends `__Secure-` iff `secureCookies`. */\n sessionCookieName?: string\n /** Mint the host auth framework's own session cookie(s); return complete\n * Set-Cookie header values (the handler appends them verbatim and sets no\n * session cookie itself). Supply this when the framework should stay\n * authoritative over name/prefix/signing/attributes — e.g. better-auth:\n * `auth.$context.authCookies.sessionToken` + `makeSignature`. */\n setSessionCookie?: (\n args: TangleSsoSessionCookieArgs,\n ) => readonly string[] | Promise<readonly string[]>\n /** HMAC-SHA256 secret the host auth framework verifies session cookies with\n * (better-auth: its `secret`). Required when `setSessionCookie` is absent —\n * the default cookie is minted to better-call's signed contract via\n * `signSessionCookieValue`; an unsigned or mis-signed value reads back as a\n * null session, so there is deliberately no fallback to `stateSecret`\n * (which is not guaranteed to be the auth secret). */\n sessionCookieSecret?: string\n /** Adds `Secure` to every cookie this module sets, and (default session\n * cookie only) the `__Secure-` name prefix. Must match the auth\n * framework's own secure-cookie decision (better-auth: https `baseURL` /\n * `advanced.useSecureCookies`), or it will look up a different cookie name\n * than the one set here. */\n secureCookies: boolean\n /** Default 604 800 (7 days). */\n sessionTtlSeconds?: number\n /** Default 600. Applies to both the cookie Max-Age and the signed TTL. */\n stateTtlSeconds?: number\n /** Default '/app'. */\n defaultRedirectPath?: string\n /** Default '/login'. */\n loginPath?: string\n /** Failure log hook (e.g. console.error). Default no-op. */\n log?: (message: string, error?: unknown) => void\n now?: () => number\n}\n\n/** Define handlers for SSO start and callback routes managing authentication flow and session cookies */\nexport interface TangleSsoHandlers {\n /** GET start route: mint + sign state, set the state cookie, 302 to the\n * platform authorize URL. `?redirect=` carries the post-login path. */\n start(request: Request): Promise<Response>\n /** GET callback route: verify state, exchange the code, upsert the user,\n * create the session, save the platform link, set the session cookie\n * (via the `setSessionCookie` seam, else signed to better-call's contract\n * with `sessionCookieSecret`), 302 to the saved redirect. Every failure\n * 302s to `loginPath?error=…` with the state cookie cleared. */\n callback(request: Request): Promise<Response>\n}\n\n/** Redirect targets must stay on this app's origin. A prefix check on `/`\n * and `//` is not enough: in the WHATWG parser a backslash is a path\n * separator for special schemes, so `/\\host` resolves to `https://host/`,\n * and tab or newline are stripped before parsing, so `/\\t/host` is `//host`.\n * Parse the value against a fixed origin and accept it only when that origin\n * survives; the result is the parser's normalised path, query and fragment.\n * Backslash and control characters are rejected up front as well, so the\n * decision does not rest on one parser's handling of them.\n *\n * The input staying on-origin is not enough either: dot-segment removal\n * turns `/.//host` or `/a/..//host` into the pathname `//host` with the\n * origin intact, and a browser resolves that Location off-origin. So the\n * emitted value must itself resolve on-origin and come back unchanged. */\nconst REDIRECT_REFERENCE_ORIGIN = 'https://redirect-reference.invalid'\n\nfunction sanitizeRedirectPath(value: string | null, fallback: string): string {\n if (!value || !value.startsWith('/') || /[\\\\\\u0000-\\u001f\\u007f]/.test(value)) return fallback\n const path = resolveOnReferenceOrigin(value)\n if (path === null || resolveOnReferenceOrigin(path) !== path) return fallback\n return path\n}\n\n/** `path + search + hash` of `value` resolved against the reference origin,\n * or null when the parse fails or lands on another origin. */\nfunction resolveOnReferenceOrigin(value: string): string | null {\n let url: URL\n try {\n url = new URL(value, REDIRECT_REFERENCE_ORIGIN)\n } catch {\n return null\n }\n if (url.origin !== REDIRECT_REFERENCE_ORIGIN) return null\n return `${url.pathname}${url.search}${url.hash}`\n}\n\nfunction redirectResponse(location: string, headers = new Headers()): Response {\n headers.set('Location', location)\n return new Response(null, { status: 302, headers })\n}\n\n/** Real client IP: `CF-Connecting-IP` behind Cloudflare, else the first\n * `x-forwarded-for` hop (the rest of the list is sender-controlled). */\nfunction clientIp(request: Request): string | null {\n return (\n request.headers.get('CF-Connecting-IP') ??\n request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ??\n null\n )\n}\n\ninterface StateCookiePayload {\n s: string\n r: string\n}\n\nfunction parseStateCookiePayload(raw: string | null): StateCookiePayload | null {\n if (!raw) return null\n try {\n const parsed = JSON.parse(raw) as unknown\n if (parsed === null || typeof parsed !== 'object') return null\n const { s, r } = parsed as Record<string, unknown>\n if (typeof s !== 'string' || typeof r !== 'string') return null\n return { s, r }\n } catch {\n return null\n }\n}\n\n/** Create Tangle SSO handlers to manage authentication state, callbacks, and session cookies */\nexport function createTangleSsoHandlers(opts: TangleSsoHandlerOptions): TangleSsoHandlers {\n if (!opts.stateSecret) throw new Error('TangleSsoHandlerOptions.stateSecret is required')\n if (!opts.callbackUrl) throw new Error('TangleSsoHandlerOptions.callbackUrl is required')\n if (!opts.stateCookieName) throw new Error('TangleSsoHandlerOptions.stateCookieName is required')\n\n const sessionCookieName = opts.sessionCookieName ?? DEFAULT_SESSION_COOKIE\n\n let mintSessionCookies: (args: TangleSsoSessionCookieArgs) => Promise<readonly string[]>\n if (opts.setSessionCookie) {\n const seam = opts.setSessionCookie\n mintSessionCookies = async (args) => await seam(args)\n } else if (opts.sessionCookieSecret) {\n const secret = opts.sessionCookieSecret\n mintSessionCookies = async ({ token, secure, ttlSeconds }) => [\n serializeCookie(await signSessionCookieValue(token, secret), {\n name: secure ? `__Secure-${sessionCookieName}` : sessionCookieName,\n secure,\n maxAgeSeconds: ttlSeconds,\n }),\n ]\n } else {\n throw new Error(\n 'TangleSsoHandlerOptions requires setSessionCookie or sessionCookieSecret: ' +\n 'better-auth only accepts HMAC-signed (and, on https, __Secure--prefixed) session cookies, ' +\n 'so an unsigned default would mint sessions that read back null',\n )\n }\n const sessionTtlSeconds = opts.sessionTtlSeconds ?? DEFAULT_SESSION_TTL_SECONDS\n const stateTtlSeconds = opts.stateTtlSeconds ?? DEFAULT_STATE_TTL_SECONDS\n const defaultRedirectPath = opts.defaultRedirectPath ?? DEFAULT_REDIRECT_PATH\n const loginPath = opts.loginPath ?? DEFAULT_LOGIN_PATH\n const log = opts.log ?? (() => {})\n const now = opts.now ?? Date.now\n const stateConfig: SsoStateConfig = { secret: opts.stateSecret, ttlMs: stateTtlSeconds * 1000, now }\n\n const stateCookieOpts = { name: opts.stateCookieName, secure: opts.secureCookies }\n\n function loginErrorRedirect(code: string): Response {\n const headers = new Headers()\n headers.append('Set-Cookie', clearCookieHeader(stateCookieOpts))\n return redirectResponse(`${loginPath}?error=${code}`, headers)\n }\n\n return {\n async start(request) {\n const url = new URL(request.url)\n const redirectPath = sanitizeRedirectPath(url.searchParams.get('redirect'), defaultRedirectPath)\n const state = await createSignedSsoState(stateConfig)\n const cookie = serializeCookie(JSON.stringify({ s: state, r: redirectPath }), {\n ...stateCookieOpts,\n maxAgeSeconds: stateTtlSeconds,\n })\n const headers = new Headers()\n headers.append('Set-Cookie', cookie)\n return redirectResponse(opts.auth.authorizeUrl({ state, redirectUri: opts.callbackUrl }), headers)\n },\n\n async callback(request) {\n const url = new URL(request.url)\n const code = url.searchParams.get('code')\n const stateFromPlatform = url.searchParams.get('state')\n if (!code || !stateFromPlatform) return loginErrorRedirect('tangle_callback_missing')\n\n const payload = parseStateCookiePayload(readCookieValue(request.headers.get('cookie'), opts.stateCookieName))\n if (!payload || payload.s !== stateFromPlatform) return loginErrorRedirect('tangle_state_mismatch')\n if (!(await verifySignedSsoState(payload.s, stateConfig))) return loginErrorRedirect('tangle_state_mismatch')\n\n let exchanged: TangleSsoExchangeResult\n try {\n exchanged = await opts.auth.exchange(code)\n } catch (err) {\n log('[tangle-sso] exchange failed', err)\n return loginErrorRedirect('tangle_exchange_failed')\n }\n if (exchanged.emailVerified !== true) {\n log('[tangle-sso] exchange did not include a verified-email proof')\n return loginErrorRedirect('tangle_exchange_failed')\n }\n\n let userId: string\n try {\n const email = normalizeTangleSsoEmail(exchanged.user.email)\n const resolution = await opts.store.resolveAccount({\n email,\n platformUserId: exchanged.user.id,\n })\n if (resolution.kind === 'reject') {\n throw new TangleSsoAccountConflictError(resolution.reason)\n }\n ;({ userId } = await opts.store.upsertUserByEmail({\n email,\n name: exchanged.user.name ?? null,\n tangleUserId: exchanged.user.id,\n resolution,\n }))\n } catch (err) {\n if (err instanceof TangleSsoAccountConflictError) {\n log('[tangle-sso] account linking conflict', err)\n return loginErrorRedirect('tangle_account_conflict')\n }\n if (err instanceof TangleSsoUserCreateError) return loginErrorRedirect('tangle_user_create_failed')\n throw err\n }\n\n const expiresAt = new Date(now() + sessionTtlSeconds * 1000)\n const { token } = await opts.store.createSession({\n userId,\n expiresAt,\n ipAddress: clientIp(request),\n userAgent: request.headers.get('user-agent'),\n })\n\n await opts.store.saveTangleLink({\n userId,\n sessionToken: token,\n tangleUserId: exchanged.user.id,\n email: normalizeTangleSsoEmail(exchanged.user.email),\n name: exchanged.user.name ?? null,\n apiKey: exchanged.apiKey,\n planTier: exchanged.plan?.tier ?? null,\n })\n\n const headers = new Headers()\n headers.append('Set-Cookie', clearCookieHeader(stateCookieOpts))\n const sessionCookies = await mintSessionCookies({\n token,\n expiresAt,\n ttlSeconds: sessionTtlSeconds,\n secure: opts.secureCookies,\n })\n for (const cookie of sessionCookies) headers.append('Set-Cookie', cookie)\n return redirectResponse(sanitizeRedirectPath(payload.r, defaultRedirectPath), headers)\n },\n }\n}\n","/**\n * Request guards for agent-app routes: session auth (302 redirect for pages,\n * JSON 401 for APIs), admin allowlisting (404 — the route stays invisible to\n * non-admins), and the billable-balance gate (402 with a stable code).\n * Session resolution is a seam; thrown Responses follow the router convention\n * of surfacing a thrown Response as the route result.\n */\n\nimport { isTangleBillingEnforcementDisabled } from '../runtime/model'\n\n/** Define options for configuring authentication guard behavior and session retrieval */\nexport interface AuthGuardOptions<Session> {\n /** e.g. a better-auth `auth.api.getSession` wrapped by the app. */\n getSession(request: Request): Promise<Session | null | undefined>\n /** Default '/login'. */\n loginPath?: string\n}\n\n/** Resolve user authentication and session requirements for page and API requests */\nexport interface AuthGuard<Session> {\n /** Page guard — throws a 302 redirect Response to `loginPath`. */\n requireUser(request: Request): Promise<Session>\n /** API guard — throws JSON 401 `{ error: 'Unauthorized', code: 'auth.unauthenticated' }`. */\n requireApiUser(request: Request): Promise<Session>\n /** `apiResponse` selects the 401 JSON path over the redirect. */\n requireSession(request: Request, opts?: { apiResponse?: boolean }): Promise<Session>\n getOptionalSession(request: Request): Promise<Session | null>\n}\n\n/** Create an authentication guard that enforces session presence and handles unauthorized access responses */\nexport function createAuthGuard<Session>(opts: AuthGuardOptions<Session>): AuthGuard<Session> {\n const loginPath = opts.loginPath ?? '/login'\n\n async function requireSession(request: Request, o: { apiResponse?: boolean } = {}): Promise<Session> {\n const session = await opts.getSession(request)\n if (!session) {\n if (o.apiResponse) {\n throw Response.json({ error: 'Unauthorized', code: 'auth.unauthenticated' }, { status: 401 })\n }\n throw new Response(null, { status: 302, headers: { Location: loginPath } })\n }\n return session\n }\n\n return {\n requireSession,\n requireUser: (request) => requireSession(request),\n requireApiUser: (request) => requireSession(request, { apiResponse: true }),\n getOptionalSession: async (request) => (await opts.getSession(request)) ?? null,\n }\n}\n\n/** Resolve a value or an HTTP response indicating failure in a guarded operation */\nexport type GuardResolution<T> = { ok: true; value: T } | { ok: false; response: Response }\n\n/**\n * Adapt a guard that THROWS a Response (the quartet above — the router\n * convention) to the `{ok: true, value} | {ok: false, response}` resolution\n * shape the route factories take (`/chat-routes` `authorize`,\n * `/interactions` `resolveConnection`, `/chat-routes` upload `authorize`).\n * Every product wrote this try/catch by hand; it lives here once.\n */\nexport async function guardResolution<T>(run: () => Promise<T>): Promise<GuardResolution<T>> {\n try {\n return { ok: true, value: await run() }\n } catch (err) {\n if (err instanceof Response) return { ok: false, response: err }\n throw err\n }\n}\n\n/** Comma/whitespace separated → trimmed, lowercased, empties dropped. */\nexport function parseAdminEmails(raw: string | null | undefined): string[] {\n return (raw ?? '')\n .split(/[,\\s]+/)\n .map((e) => e.trim().toLowerCase())\n .filter(Boolean)\n}\n\n/** Define options to resolve user session and control access based on allowed admin emails */\nexport interface AdminGuardOptions<Session> {\n requireUser(request: Request): Promise<Session>\n emailOf(session: Session): string | null | undefined\n /** Resolved per request; an EMPTY allowlist refuses everyone. */\n allowedEmails(): string[]\n}\n\n/** Non-admins (and empty allowlists) get 404, keeping the route invisible —\n * better than a \"forbidden\" footprint that advertises its existence. */\nexport function createAdminGuard<Session>(opts: AdminGuardOptions<Session>): (request: Request) => Promise<Session> {\n return async (request) => {\n const session = await opts.requireUser(request)\n const allowed = opts.allowedEmails()\n if (allowed.length === 0) throw new Response('Not found', { status: 404 })\n const email = (opts.emailOf(session) ?? '').toLowerCase()\n if (!allowed.includes(email)) throw new Response('Not found', { status: 404 })\n return session\n }\n}\n\n/** Describe the billable balance state including overage permission and remaining USD balance */\nexport interface BillableBalanceState {\n overageAllowed: boolean\n remainingBalanceUsd: number\n}\n\n/** Define options to assert and customize billable balance enforcement behavior */\nexport interface AssertBillableBalanceOptions {\n env?: Record<string, string | undefined>\n /** App-specific enforcement override flag (e.g. 'GTM_BILLING_ENFORCEMENT'),\n * fed to `isTangleBillingEnforcementDisabled`. */\n enforcementEnvVar?: string\n /** Default 'Add balance or upgrade your plan to invoke this agent.'. */\n errorMessage?: string\n /** Merged into the 402 JSON body (e.g. `{ organizationId }`). */\n errorBody?: Record<string, unknown>\n}\n\n/**\n * Gate a billable turn: passes when enforcement is disabled (dev default),\n * the tier allows overage, or remaining balance is positive. Otherwise throws\n * a 402 Response with the stable `billing.balance_required` code so clients\n * can route to the billing screen.\n */\nexport function assertBillableBalance(state: BillableBalanceState, opts: AssertBillableBalanceOptions = {}): void {\n if (isTangleBillingEnforcementDisabled({ env: opts.env, enforcementEnvVar: opts.enforcementEnvVar })) return\n if (state.overageAllowed || state.remainingBalanceUsd > 0) return\n // errorBody first: the stable error/code contract always wins over caller extras.\n throw Response.json(\n {\n ...opts.errorBody,\n error: opts.errorMessage ?? 'Add balance or upgrade your plan to invoke this agent.',\n code: 'billing.balance_required',\n },\n { status: 402 },\n )\n}\n"],"mappings":"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","names":[]}
|
|
@@ -43,6 +43,10 @@ import {
|
|
|
43
43
|
selectTickStep
|
|
44
44
|
} from "../chunk-KWN5PGXM.js";
|
|
45
45
|
import "../chunk-Q2QKEV6J.js";
|
|
46
|
+
import {
|
|
47
|
+
DesignCanvasChromeLazy,
|
|
48
|
+
DesignCanvasLazy
|
|
49
|
+
} from "../chunk-WAMJSZYB.js";
|
|
46
50
|
import "../chunk-ZN5J47UX.js";
|
|
47
51
|
import {
|
|
48
52
|
DEFAULT_INSERT_TEMPLATES,
|
|
@@ -90,10 +94,6 @@ import {
|
|
|
90
94
|
bleedAwareExportBounds,
|
|
91
95
|
scaleForPreset
|
|
92
96
|
} from "../chunk-BWQPVS7D.js";
|
|
93
|
-
import {
|
|
94
|
-
DesignCanvasChromeLazy,
|
|
95
|
-
DesignCanvasLazy
|
|
96
|
-
} from "../chunk-WAMJSZYB.js";
|
|
97
97
|
import "../chunk-EA4UVS4T.js";
|
|
98
98
|
import "../chunk-TXD5HXLE.js";
|
|
99
99
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { type HubProviderEvent } from '@tangle-network/hub-sdk';
|
|
2
|
+
import type { AgentProfile } from '@tangle-network/sandbox';
|
|
3
|
+
/**
|
|
4
|
+
* A hosted agent: a person texts or calls a line, and the agent answers from
|
|
5
|
+
* that person's own isolated sandbox. The developer's Tangle API key pays for
|
|
6
|
+
* every box, model turn and reply.
|
|
7
|
+
*
|
|
8
|
+
* Each person gets one fresh isolated box built from the published profile,
|
|
9
|
+
* never a copy of the developer's box. Later messages resume the same box and
|
|
10
|
+
* the same conversation, so text and voice share one memory. A box the
|
|
11
|
+
* platform deleted is replaced by a fresh one instead of blocking the person.
|
|
12
|
+
*
|
|
13
|
+
* Every step is idempotent by the message's turn id: a retried delivery
|
|
14
|
+
* settles the same sandbox turn and replays the same Hub send. So the caller
|
|
15
|
+
* may retry any failure, and needs no dedupe table.
|
|
16
|
+
*/
|
|
17
|
+
/** Durable key-value storage. A Cloudflare KV namespace satisfies it. */
|
|
18
|
+
export interface HostedAgentStore {
|
|
19
|
+
get(key: string): Promise<string | null>;
|
|
20
|
+
put(key: string, value: string, options?: {
|
|
21
|
+
expirationTtl?: number;
|
|
22
|
+
}): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export type HostedChannel = 'imessage' | 'voice';
|
|
25
|
+
export interface HostedMessage {
|
|
26
|
+
/** The sender's address as the channel delivered it: E.164 for a phone. */
|
|
27
|
+
userId: string;
|
|
28
|
+
channel: HostedChannel;
|
|
29
|
+
text: string;
|
|
30
|
+
/** Stable per logical message. Reusing it settles the same turn. */
|
|
31
|
+
turnId: string;
|
|
32
|
+
}
|
|
33
|
+
/** What the allowance hook decides for one new message. */
|
|
34
|
+
export type Allowance = 'answer' | 'ignore' | {
|
|
35
|
+
reply: string;
|
|
36
|
+
};
|
|
37
|
+
export interface HostedAgentConfig {
|
|
38
|
+
/** The developer's Tangle API key (Sandbox and Hub). */
|
|
39
|
+
apiKey: string;
|
|
40
|
+
/** The persona, model and tools every person's box runs. */
|
|
41
|
+
profile: AgentProfile;
|
|
42
|
+
/** Backend harness type, such as `opencode`; the runtime default when omitted. */
|
|
43
|
+
harness?: string;
|
|
44
|
+
store: HostedAgentStore;
|
|
45
|
+
/** Answers per person per UTC day before `allow` must decide. Default 20. */
|
|
46
|
+
freeTurnsPerDay?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Decide a new message after the free allowance is used, for example reply
|
|
49
|
+
* with a checkout link. Without it, a person past the allowance is told to
|
|
50
|
+
* come back tomorrow. Retries of an admitted message never reach this hook.
|
|
51
|
+
*/
|
|
52
|
+
allow?: (message: HostedMessage, usedToday: number) => Allowance | Promise<Allowance>;
|
|
53
|
+
/** Shared secret for the ph0ny call hook and `ask_workspace` webhook tool. */
|
|
54
|
+
voiceSecret?: string;
|
|
55
|
+
/** How long one `ask_workspace` call may wait before it returns a ticket. Default 8 s. */
|
|
56
|
+
voiceBudgetMs?: number;
|
|
57
|
+
box?: Partial<BoxPolicy>;
|
|
58
|
+
turnWallCapMs?: number;
|
|
59
|
+
sandboxUrl?: string;
|
|
60
|
+
hubUrl?: string;
|
|
61
|
+
}
|
|
62
|
+
export interface BoxPolicy {
|
|
63
|
+
cpuCores: number;
|
|
64
|
+
memoryMB: number;
|
|
65
|
+
diskGB: number;
|
|
66
|
+
idleTimeoutSeconds: number;
|
|
67
|
+
maxLifetimeSeconds: number;
|
|
68
|
+
deleteAfterStoppedSeconds: number;
|
|
69
|
+
/** Egress allow-list. The default reaches the model router only. */
|
|
70
|
+
allowDomains: string[];
|
|
71
|
+
}
|
|
72
|
+
export declare const DEFAULT_BOX_POLICY: BoxPolicy;
|
|
73
|
+
export type AskResult = {
|
|
74
|
+
state: 'answered';
|
|
75
|
+
text: string;
|
|
76
|
+
} | {
|
|
77
|
+
state: 'declined';
|
|
78
|
+
reply?: string;
|
|
79
|
+
} | {
|
|
80
|
+
state: 'pending';
|
|
81
|
+
};
|
|
82
|
+
/** A Hub delivery that passed authentication, in a form a queue can carry. */
|
|
83
|
+
export interface HostedInbound {
|
|
84
|
+
runId: string;
|
|
85
|
+
connectionId: string;
|
|
86
|
+
event: HubProviderEvent;
|
|
87
|
+
}
|
|
88
|
+
export declare const NOTICE: {
|
|
89
|
+
readonly stopped: 'You are unsubscribed. Text START to talk again.';
|
|
90
|
+
readonly started: 'Welcome back. Text me anytime. Reply STOP to stop.';
|
|
91
|
+
readonly limit: 'You have used today\'s free messages. Talk tomorrow!';
|
|
92
|
+
readonly unavailable: 'Sorry, I could not answer that just now. Please try again in a minute.';
|
|
93
|
+
};
|
|
94
|
+
/** One address per person: E.164 for anything phone-shaped, else lowercase. */
|
|
95
|
+
export declare function normalizeAddress(value: string): string | null;
|
|
96
|
+
export declare class HostedAgentError extends Error {
|
|
97
|
+
readonly code: string;
|
|
98
|
+
constructor(code: string, message: string);
|
|
99
|
+
}
|
|
100
|
+
export declare function createHostedAgent(config: HostedAgentConfig): {
|
|
101
|
+
ask: (message: HostedMessage, options: {
|
|
102
|
+
deadline: number;
|
|
103
|
+
}) => Promise<AskResult>;
|
|
104
|
+
/**
|
|
105
|
+
* One-time setup: route an Inkbox iMessage identity's messages, connected
|
|
106
|
+
* to Hub under the developer's account, to `callbackUrl`, and allow the
|
|
107
|
+
* one reply action on that connection. Safe to repeat.
|
|
108
|
+
*/
|
|
109
|
+
connect(input: {
|
|
110
|
+
connectionId: string;
|
|
111
|
+
identityId: string;
|
|
112
|
+
callbackUrl: string;
|
|
113
|
+
secret: string;
|
|
114
|
+
}): Promise<import("@tangle-network/hub-sdk").HubEventSubscription>;
|
|
115
|
+
/** The Hub callback. Authenticates within Hub's 10 s budget and runs nothing. */
|
|
116
|
+
receive(request: Request, secret: string): Promise<{
|
|
117
|
+
response: Response;
|
|
118
|
+
inbound?: HostedInbound;
|
|
119
|
+
}>;
|
|
120
|
+
/**
|
|
121
|
+
* Answer one inbound message and send the reply through Hub. Returns
|
|
122
|
+
* 'pending' while the turn still runs; call again with the same inbound.
|
|
123
|
+
* On `lastAttempt`, a failure is answered with an apology instead of thrown.
|
|
124
|
+
*/
|
|
125
|
+
respond(inbound: HostedInbound, options?: {
|
|
126
|
+
lastAttempt?: boolean;
|
|
127
|
+
}): Promise<'replied' | 'ignored' | 'pending'>;
|
|
128
|
+
/**
|
|
129
|
+
* ph0ny's call hook. `admit` returns a call token bound to the caller's
|
|
130
|
+
* number from the carrier's signed call record, never from the model.
|
|
131
|
+
*/
|
|
132
|
+
voiceHook(request: Request): Promise<Response>;
|
|
133
|
+
/**
|
|
134
|
+
* ph0ny's `ask_workspace` webhook tool, configured with `forwardCallToken`.
|
|
135
|
+
* A slow turn returns a ticket; the voice agent asks again with it.
|
|
136
|
+
*/
|
|
137
|
+
voiceAsk(request: Request): Promise<Response>;
|
|
138
|
+
};
|
|
139
|
+
export type HostedAgent = ReturnType<typeof createHostedAgent>;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// src/hosted-agent/index.ts
|
|
2
|
+
import { authenticateHubEventRequest, HubClient } from "@tangle-network/hub-sdk";
|
|
3
|
+
import { buildMessagingReply, normalizeConversationEvent } from "@tangle-network/agent-integrations/conversation-events";
|
|
4
|
+
import { Sandbox } from "@tangle-network/sandbox/core";
|
|
5
|
+
var DEFAULT_BOX_POLICY = {
|
|
6
|
+
cpuCores: 1,
|
|
7
|
+
memoryMB: 2048,
|
|
8
|
+
diskGB: 10,
|
|
9
|
+
idleTimeoutSeconds: 600,
|
|
10
|
+
maxLifetimeSeconds: 86400,
|
|
11
|
+
deleteAfterStoppedSeconds: 7 * 86400,
|
|
12
|
+
allowDomains: ["router.tangle.tools"]
|
|
13
|
+
};
|
|
14
|
+
var NOTICE = {
|
|
15
|
+
stopped: "You are unsubscribed. Text START to talk again.",
|
|
16
|
+
started: "Welcome back. Text me anytime. Reply STOP to stop.",
|
|
17
|
+
limit: "You have used today's free messages. Talk tomorrow!",
|
|
18
|
+
unavailable: "Sorry, I could not answer that just now. Please try again in a minute."
|
|
19
|
+
};
|
|
20
|
+
var IMESSAGE_EVENT = "inkbox.imessage.received";
|
|
21
|
+
var IMESSAGE_REPLY = "inkbox.imessage.reply";
|
|
22
|
+
var STOP = /^(stop|stopall|unsubscribe|cancel|end|quit)$/i;
|
|
23
|
+
var START = /^(start|unstop|resume)$/i;
|
|
24
|
+
var TWO_DAYS = 2 * 86400;
|
|
25
|
+
var RESUMABLE = /* @__PURE__ */ new Set(["stopped", "expired"]);
|
|
26
|
+
var GONE = /* @__PURE__ */ new Set(["failed", "deleted"]);
|
|
27
|
+
var encode = (text) => new TextEncoder().encode(text);
|
|
28
|
+
var hex = (bytes) => Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
29
|
+
var sha256 = async (value) => hex(await crypto.subtle.digest("SHA-256", encode(value)));
|
|
30
|
+
async function hmac(secret, value) {
|
|
31
|
+
const key = await crypto.subtle.importKey("raw", encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
32
|
+
return hex(await crypto.subtle.sign("HMAC", key, encode(value)));
|
|
33
|
+
}
|
|
34
|
+
async function equal(given, expected) {
|
|
35
|
+
if (!given) return false;
|
|
36
|
+
const [a, b] = await Promise.all([sha256(given), sha256(expected)]);
|
|
37
|
+
let diff = 0;
|
|
38
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
39
|
+
return diff === 0;
|
|
40
|
+
}
|
|
41
|
+
var b64 = (text) => btoa(String.fromCharCode(...encode(text))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
42
|
+
var unb64 = (text) => new TextDecoder().decode(Uint8Array.from(atob(text.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0)));
|
|
43
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
44
|
+
var json = (body, status = 200) => Response.json(body, { status });
|
|
45
|
+
var describe = (error) => (error instanceof Error ? `${error.name}: ${error.message}` : String(error)).replace(/sk-tan-[\w-]+|Bearer\s+\S+/g, "<redacted>").slice(0, 400);
|
|
46
|
+
function normalizeAddress(value) {
|
|
47
|
+
const trimmed = value.trim();
|
|
48
|
+
if (trimmed.includes("@")) return trimmed.toLowerCase();
|
|
49
|
+
const digits = trimmed.replace(/[^\d]/g, "");
|
|
50
|
+
if (digits.length < 8 || digits.length > 15) return null;
|
|
51
|
+
return `+${digits.length === 10 && !trimmed.startsWith("+") ? `1${digits}` : digits}`;
|
|
52
|
+
}
|
|
53
|
+
var HostedAgentError = class extends Error {
|
|
54
|
+
constructor(code, message) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.code = code;
|
|
57
|
+
this.name = "HostedAgentError";
|
|
58
|
+
}
|
|
59
|
+
code;
|
|
60
|
+
};
|
|
61
|
+
function createHostedAgent(config) {
|
|
62
|
+
const store = config.store;
|
|
63
|
+
const policy = { ...DEFAULT_BOX_POLICY, ...config.box };
|
|
64
|
+
const wallCapMs = config.turnWallCapMs ?? 12e4;
|
|
65
|
+
const sandbox = new Sandbox({ apiKey: config.apiKey, baseUrl: config.sandboxUrl ?? "https://sandbox.tangle.tools", timeoutMs: 2e4 });
|
|
66
|
+
const hub = new HubClient({ baseUrl: config.hubUrl ?? "https://id.tangle.tools", apiKey: config.apiKey });
|
|
67
|
+
const backend = { ...config.harness ? { type: config.harness } : {}, profile: config.profile };
|
|
68
|
+
const profileTag = sha256(JSON.stringify(config.profile)).then((hash) => hash.slice(0, 8));
|
|
69
|
+
async function ensureBox(user, deadline) {
|
|
70
|
+
const key = `box:${user}`;
|
|
71
|
+
const known = await store.get(key);
|
|
72
|
+
let box = known ? await sandbox.get(known) : null;
|
|
73
|
+
if (!box || GONE.has(box.status)) {
|
|
74
|
+
box = await sandbox.createIsolated({
|
|
75
|
+
name: `hosted-${user.slice(0, 24)}`,
|
|
76
|
+
idempotencyKey: known ? `hosted-${user}-after-${known}` : `hosted-${user}`,
|
|
77
|
+
resources: { cpuCores: policy.cpuCores, memoryMB: policy.memoryMB, diskGB: policy.diskGB },
|
|
78
|
+
backend,
|
|
79
|
+
secrets: [],
|
|
80
|
+
sshEnabled: false,
|
|
81
|
+
egressPolicy: { mode: "strict", allowDomains: policy.allowDomains, includeImplicitDomains: false },
|
|
82
|
+
idleTimeoutSeconds: policy.idleTimeoutSeconds,
|
|
83
|
+
maxLifetimeSeconds: policy.maxLifetimeSeconds,
|
|
84
|
+
deleteAfterStoppedSeconds: policy.deleteAfterStoppedSeconds,
|
|
85
|
+
metadata: { hostedUser: user }
|
|
86
|
+
});
|
|
87
|
+
await store.put(key, box.id);
|
|
88
|
+
}
|
|
89
|
+
if (RESUMABLE.has(box.status)) await box.resume();
|
|
90
|
+
if (box.status === "running") return box;
|
|
91
|
+
const timeoutMs = deadline - Date.now();
|
|
92
|
+
if (timeoutMs < 1e3) return null;
|
|
93
|
+
try {
|
|
94
|
+
return await sandbox.waitForRunning(box.id, { timeoutMs });
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (error instanceof Error && error.name === "TimeoutError") return null;
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function admit(message, user) {
|
|
101
|
+
const stopKey = `stop:${user}`;
|
|
102
|
+
if (message.channel !== "voice" && STOP.test(message.text.trim())) {
|
|
103
|
+
await store.put(stopKey, "1");
|
|
104
|
+
return { reply: NOTICE.stopped };
|
|
105
|
+
}
|
|
106
|
+
if (message.channel !== "voice" && START.test(message.text.trim())) {
|
|
107
|
+
await store.put(stopKey, "");
|
|
108
|
+
return { reply: NOTICE.started };
|
|
109
|
+
}
|
|
110
|
+
if (await store.get(stopKey)) return "ignore";
|
|
111
|
+
const dayKey = `turns:${user}:${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
|
|
112
|
+
const used = Number(await store.get(dayKey) ?? 0);
|
|
113
|
+
const decision = used < (config.freeTurnsPerDay ?? 20) ? "answer" : config.allow ? await config.allow(message, used) : { reply: NOTICE.limit };
|
|
114
|
+
if (decision === "answer") await store.put(dayKey, String(used + 1), { expirationTtl: TWO_DAYS });
|
|
115
|
+
return decision;
|
|
116
|
+
}
|
|
117
|
+
async function ask(message, options) {
|
|
118
|
+
const text = message.text.trim();
|
|
119
|
+
if (!text || text.length > 8e3 || !/^[\w-]{1,120}$/.test(message.turnId)) return { state: "declined" };
|
|
120
|
+
const user = (await sha256(message.userId)).slice(0, 32);
|
|
121
|
+
const admittedKey = `turn:${user}:${message.turnId}`;
|
|
122
|
+
if (!await store.get(admittedKey)) {
|
|
123
|
+
const decision = await admit(message, user);
|
|
124
|
+
if (decision === "ignore") return { state: "declined" };
|
|
125
|
+
if (decision !== "answer") return { state: "declined", reply: decision.reply };
|
|
126
|
+
await store.put(admittedKey, "1", { expirationTtl: TWO_DAYS });
|
|
127
|
+
}
|
|
128
|
+
const box = await ensureBox(user, options.deadline);
|
|
129
|
+
if (!box) return { state: "pending" };
|
|
130
|
+
const sessionId = `hosted-${user}-${await profileTag}`;
|
|
131
|
+
if (!await box.session(sessionId).status()) await box.createSession({ sessionId, retention: "workspace", backend });
|
|
132
|
+
const prompt = message.channel === "voice" ? `[Phone call. Answer in one to three short spoken sentences.]
|
|
133
|
+
${text}` : text;
|
|
134
|
+
for (; ; ) {
|
|
135
|
+
const result = await box.driveConversationTurn(prompt, { sessionId, turnId: message.turnId, wallCapMs, timeoutMs: 8e3 });
|
|
136
|
+
if (result.state === "completed") {
|
|
137
|
+
const answer = result.text.trim();
|
|
138
|
+
if (!answer) throw new HostedAgentError("empty_reply", "The turn completed with no reply text.");
|
|
139
|
+
return { state: "answered", text: answer };
|
|
140
|
+
}
|
|
141
|
+
if (result.state === "failed") throw new HostedAgentError("turn_failed", result.error);
|
|
142
|
+
if (Date.now() + 2e3 > options.deadline) return { state: "pending" };
|
|
143
|
+
await sleep(2e3);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
ask,
|
|
148
|
+
/**
|
|
149
|
+
* One-time setup: route an Inkbox iMessage identity's messages, connected
|
|
150
|
+
* to Hub under the developer's account, to `callbackUrl`, and allow the
|
|
151
|
+
* one reply action on that connection. Safe to repeat.
|
|
152
|
+
*/
|
|
153
|
+
async connect(input) {
|
|
154
|
+
const webhook = await hub.connections.inboundWebhook.get(input.connectionId);
|
|
155
|
+
if (!webhook.configured) await hub.connections.inboundWebhook.create(input.connectionId, { identityId: input.identityId, imessage: true });
|
|
156
|
+
await hub.permissions.set({ connectionId: input.connectionId, actionPath: IMESSAGE_REPLY, decision: "allow" });
|
|
157
|
+
const { subscription } = await hub.eventSubscriptions.create({
|
|
158
|
+
clientReference: `hosted-agent:${input.connectionId}`,
|
|
159
|
+
label: "Hosted agent messages",
|
|
160
|
+
source: { type: "connection", connectionId: input.connectionId },
|
|
161
|
+
event: IMESSAGE_EVENT,
|
|
162
|
+
callback: { url: input.callbackUrl, secret: input.secret }
|
|
163
|
+
});
|
|
164
|
+
return subscription;
|
|
165
|
+
},
|
|
166
|
+
/** The Hub callback. Authenticates within Hub's 10 s budget and runs nothing. */
|
|
167
|
+
async receive(request, secret) {
|
|
168
|
+
const authenticated = await authenticateHubEventRequest({ request, secret, maxBodyBytes: 64 * 1024 });
|
|
169
|
+
if (!authenticated.ok) return { response: authenticated.response };
|
|
170
|
+
const { runId, source, providerEvent } = authenticated.delivery;
|
|
171
|
+
const ack = new Response(null, { status: 204 });
|
|
172
|
+
if (source.kind !== "connection" || source.event !== IMESSAGE_EVENT || providerEvent.connectionId !== source.id) return { response: ack };
|
|
173
|
+
return { response: ack, inbound: { runId, connectionId: source.id, event: providerEvent } };
|
|
174
|
+
},
|
|
175
|
+
/**
|
|
176
|
+
* Answer one inbound message and send the reply through Hub. Returns
|
|
177
|
+
* 'pending' while the turn still runs; call again with the same inbound.
|
|
178
|
+
* On `lastAttempt`, a failure is answered with an apology instead of thrown.
|
|
179
|
+
*/
|
|
180
|
+
async respond(inbound, options = {}) {
|
|
181
|
+
const normalized = normalizeConversationEvent(inbound.event);
|
|
182
|
+
if (!normalized.ok) return "ignored";
|
|
183
|
+
const event = normalized.event;
|
|
184
|
+
const userId = normalizeAddress(event.sender.id ?? "");
|
|
185
|
+
if (!userId || !event.text?.trim() || event.isGroup || event.historyOnly) return "ignored";
|
|
186
|
+
const turnId = `t-${(await sha256(inbound.runId)).slice(0, 40)}`;
|
|
187
|
+
let text;
|
|
188
|
+
try {
|
|
189
|
+
const result = await ask(
|
|
190
|
+
{ userId, channel: "imessage", text: event.text, turnId },
|
|
191
|
+
{ deadline: Date.now() + wallCapMs + 6e4 }
|
|
192
|
+
);
|
|
193
|
+
if (result.state === "pending") return "pending";
|
|
194
|
+
if (result.state === "declined" && !result.reply) return "ignored";
|
|
195
|
+
text = result.state === "answered" ? result.text : result.reply;
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (!(error instanceof HostedAgentError) && !options.lastAttempt) throw error;
|
|
198
|
+
console.error(`[hosted-agent] turn=${turnId} ${describe(error)}`);
|
|
199
|
+
text = NOTICE.unavailable;
|
|
200
|
+
}
|
|
201
|
+
const plan = buildMessagingReply(inbound.event, text.slice(0, 1500), `reply-${turnId}`);
|
|
202
|
+
if (!plan.ok) return "ignored";
|
|
203
|
+
await hub.tools.invoke(plan.reply.action, plan.reply.input, { connectionId: inbound.connectionId, idempotencyKey: plan.reply.idempotencyKey });
|
|
204
|
+
return "replied";
|
|
205
|
+
},
|
|
206
|
+
/**
|
|
207
|
+
* ph0ny's call hook. `admit` returns a call token bound to the caller's
|
|
208
|
+
* number from the carrier's signed call record, never from the model.
|
|
209
|
+
*/
|
|
210
|
+
async voiceHook(request) {
|
|
211
|
+
const secret = config.voiceSecret;
|
|
212
|
+
if (!secret || !await equal(request.headers.get("authorization"), `Bearer ${secret}`)) return json({ error: "unauthorized" }, 401);
|
|
213
|
+
const body = await request.json().catch(() => null);
|
|
214
|
+
if (body?.event !== "admit") return json({ ok: true });
|
|
215
|
+
const phone = normalizeAddress(body.phone ?? "");
|
|
216
|
+
if (!phone?.startsWith("+")) return json({ admit: false, say: "Sorry, I can only take calls from a visible number." });
|
|
217
|
+
if (await store.get(`stop:${(await sha256(phone)).slice(0, 32)}`)) return json({ admit: false, say: "You unsubscribed from this line." });
|
|
218
|
+
const claims = b64(JSON.stringify({ phone, exp: Date.now() + 2 * 36e5 }));
|
|
219
|
+
return json({ admit: true, callToken: `hvt.${claims}.${await hmac(secret, claims)}` });
|
|
220
|
+
},
|
|
221
|
+
/**
|
|
222
|
+
* ph0ny's `ask_workspace` webhook tool, configured with `forwardCallToken`.
|
|
223
|
+
* A slow turn returns a ticket; the voice agent asks again with it.
|
|
224
|
+
*/
|
|
225
|
+
async voiceAsk(request) {
|
|
226
|
+
const secret = config.voiceSecret;
|
|
227
|
+
if (!secret || !await equal(request.headers.get("authorization"), `Bearer ${secret}`)) return json({ error: "unauthorized" }, 401);
|
|
228
|
+
const [tag, claims, signature] = (request.headers.get("x-voice-call-token") ?? "").split(".");
|
|
229
|
+
if (tag !== "hvt" || !claims || !await equal(signature, await hmac(secret, claims))) return json({ status: "error", error: "call_not_admitted" }, 403);
|
|
230
|
+
const call = JSON.parse(unb64(claims));
|
|
231
|
+
if (call.exp < Date.now()) return json({ status: "error", error: "call_expired" }, 403);
|
|
232
|
+
const body = await request.json().catch(() => null);
|
|
233
|
+
const [turnId, asked] = body?.ticket ? body.ticket.split(".") : [`v-${crypto.randomUUID()}`, b64(body?.utterance ?? "")];
|
|
234
|
+
if (!turnId || !asked) return json({ status: "error", error: "missing_utterance" }, 400);
|
|
235
|
+
try {
|
|
236
|
+
const result = await ask({ userId: call.phone, channel: "voice", text: unb64(asked), turnId }, { deadline: Date.now() + (config.voiceBudgetMs ?? 8e3) });
|
|
237
|
+
if (result.state === "pending") return json({ status: "pending", ticket: `${turnId}.${asked}` });
|
|
238
|
+
if (result.state === "declined") return json({ status: "complete", answer: result.reply ?? "I cannot answer that right now." });
|
|
239
|
+
return json({ status: "complete", answer: result.text });
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.error(`[hosted-agent] voice turn=${turnId} ${describe(error)}`);
|
|
242
|
+
return json({ status: "error", error: "agent_unavailable" }, 502);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
export {
|
|
248
|
+
DEFAULT_BOX_POLICY,
|
|
249
|
+
HostedAgentError,
|
|
250
|
+
NOTICE,
|
|
251
|
+
createHostedAgent,
|
|
252
|
+
normalizeAddress
|
|
253
|
+
};
|
|
254
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/hosted-agent/index.ts"],"sourcesContent":["import { authenticateHubEventRequest, HubClient, type HubProviderEvent } from '@tangle-network/hub-sdk'\nimport { buildMessagingReply, normalizeConversationEvent } from '@tangle-network/agent-integrations/conversation-events'\nimport type { AgentProfile, BackendConfig, SandboxInstance } from '@tangle-network/sandbox'\nimport { Sandbox } from '@tangle-network/sandbox/core'\n\n/**\n * A hosted agent: a person texts or calls a line, and the agent answers from\n * that person's own isolated sandbox. The developer's Tangle API key pays for\n * every box, model turn and reply.\n *\n * Each person gets one fresh isolated box built from the published profile,\n * never a copy of the developer's box. Later messages resume the same box and\n * the same conversation, so text and voice share one memory. A box the\n * platform deleted is replaced by a fresh one instead of blocking the person.\n *\n * Every step is idempotent by the message's turn id: a retried delivery\n * settles the same sandbox turn and replays the same Hub send. So the caller\n * may retry any failure, and needs no dedupe table.\n */\n\n/** Durable key-value storage. A Cloudflare KV namespace satisfies it. */\nexport interface HostedAgentStore {\n get(key: string): Promise<string | null>\n put(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>\n}\n\nexport type HostedChannel = 'imessage' | 'voice'\n\nexport interface HostedMessage {\n /** The sender's address as the channel delivered it: E.164 for a phone. */\n userId: string\n channel: HostedChannel\n text: string\n /** Stable per logical message. Reusing it settles the same turn. */\n turnId: string\n}\n\n/** What the allowance hook decides for one new message. */\nexport type Allowance = 'answer' | 'ignore' | { reply: string }\n\nexport interface HostedAgentConfig {\n /** The developer's Tangle API key (Sandbox and Hub). */\n apiKey: string\n /** The persona, model and tools every person's box runs. */\n profile: AgentProfile\n /** Backend harness type, such as `opencode`; the runtime default when omitted. */\n harness?: string\n store: HostedAgentStore\n /** Answers per person per UTC day before `allow` must decide. Default 20. */\n freeTurnsPerDay?: number\n /**\n * Decide a new message after the free allowance is used, for example reply\n * with a checkout link. Without it, a person past the allowance is told to\n * come back tomorrow. Retries of an admitted message never reach this hook.\n */\n allow?: (message: HostedMessage, usedToday: number) => Allowance | Promise<Allowance>\n /** Shared secret for the ph0ny call hook and `ask_workspace` webhook tool. */\n voiceSecret?: string\n /** How long one `ask_workspace` call may wait before it returns a ticket. Default 8 s. */\n voiceBudgetMs?: number\n box?: Partial<BoxPolicy>\n turnWallCapMs?: number\n sandboxUrl?: string\n hubUrl?: string\n}\n\nexport interface BoxPolicy {\n cpuCores: number\n memoryMB: number\n diskGB: number\n idleTimeoutSeconds: number\n maxLifetimeSeconds: number\n deleteAfterStoppedSeconds: number\n /** Egress allow-list. The default reaches the model router only. */\n allowDomains: string[]\n}\n\nexport const DEFAULT_BOX_POLICY: BoxPolicy = {\n cpuCores: 1, memoryMB: 2048, diskGB: 10,\n idleTimeoutSeconds: 600, maxLifetimeSeconds: 86_400, deleteAfterStoppedSeconds: 7 * 86_400,\n allowDomains: ['router.tangle.tools'],\n}\n\nexport type AskResult =\n | { state: 'answered'; text: string }\n | { state: 'declined'; reply?: string }\n | { state: 'pending' }\n\n/** A Hub delivery that passed authentication, in a form a queue can carry. */\nexport interface HostedInbound {\n runId: string\n connectionId: string\n event: HubProviderEvent\n}\n\nexport const NOTICE = {\n stopped: 'You are unsubscribed. Text START to talk again.',\n started: 'Welcome back. Text me anytime. Reply STOP to stop.',\n limit: 'You have used today\\'s free messages. Talk tomorrow!',\n unavailable: 'Sorry, I could not answer that just now. Please try again in a minute.',\n} as const\n\nconst IMESSAGE_EVENT = 'inkbox.imessage.received'\nconst IMESSAGE_REPLY = 'inkbox.imessage.reply'\nconst STOP = /^(stop|stopall|unsubscribe|cancel|end|quit)$/i\nconst START = /^(start|unstop|resume)$/i\nconst TWO_DAYS = 2 * 86_400\nconst RESUMABLE = new Set(['stopped', 'expired'])\nconst GONE = new Set(['failed', 'deleted'])\n\nconst encode = (text: string) => new TextEncoder().encode(text)\nconst hex = (bytes: ArrayBuffer) => Array.from(new Uint8Array(bytes), byte => byte.toString(16).padStart(2, '0')).join('')\nconst sha256 = async (value: string) => hex(await crypto.subtle.digest('SHA-256', encode(value)))\nasync function hmac(secret: string, value: string): Promise<string> {\n const key = await crypto.subtle.importKey('raw', encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])\n return hex(await crypto.subtle.sign('HMAC', key, encode(value)))\n}\n/** Compares digests, so the time taken reveals nothing about the secret. */\nasync function equal(given: string | null | undefined, expected: string): Promise<boolean> {\n if (!given) return false\n const [a, b] = await Promise.all([sha256(given), sha256(expected)])\n let diff = 0\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i)\n return diff === 0\n}\nconst b64 = (text: string) => btoa(String.fromCharCode(...encode(text))).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\nconst unb64 = (text: string) => new TextDecoder().decode(Uint8Array.from(atob(text.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0)))\nconst sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))\nconst json = (body: unknown, status = 200) => Response.json(body, { status })\nconst describe = (error: unknown) => (error instanceof Error ? `${error.name}: ${error.message}` : String(error))\n .replace(/sk-tan-[\\w-]+|Bearer\\s+\\S+/g, '<redacted>').slice(0, 400)\n\n/** One address per person: E.164 for anything phone-shaped, else lowercase. */\nexport function normalizeAddress(value: string): string | null {\n const trimmed = value.trim()\n if (trimmed.includes('@')) return trimmed.toLowerCase()\n const digits = trimmed.replace(/[^\\d]/g, '')\n if (digits.length < 8 || digits.length > 15) return null\n return `+${digits.length === 10 && !trimmed.startsWith('+') ? `1${digits}` : digits}`\n}\n\nexport class HostedAgentError extends Error {\n constructor(readonly code: string, message: string) { super(message); this.name = 'HostedAgentError' }\n}\n\nexport function createHostedAgent(config: HostedAgentConfig) {\n const store = config.store\n const policy = { ...DEFAULT_BOX_POLICY, ...config.box }\n const wallCapMs = config.turnWallCapMs ?? 120_000\n const sandbox = new Sandbox({ apiKey: config.apiKey, baseUrl: config.sandboxUrl ?? 'https://sandbox.tangle.tools', timeoutMs: 20_000 })\n const hub = new HubClient({ baseUrl: config.hubUrl ?? 'https://id.tangle.tools', apiKey: config.apiKey })\n const backend: BackendConfig = { ...(config.harness ? { type: config.harness as BackendConfig['type'] } : {}), profile: config.profile }\n const profileTag = sha256(JSON.stringify(config.profile)).then(hash => hash.slice(0, 8))\n\n /** The person's running box: created on the first message, resumed later,\n * replaced when the platform deleted it. Null when the deadline passed. */\n async function ensureBox(user: string, deadline: number): Promise<SandboxInstance | null> {\n const key = `box:${user}`\n const known = await store.get(key)\n let box = known ? await sandbox.get(known) : null\n if (!box || GONE.has(box.status)) {\n // The key names the box it replaces, so concurrent first messages and\n // retries converge on one allocation instead of each creating a box.\n box = await sandbox.createIsolated({\n name: `hosted-${user.slice(0, 24)}`, idempotencyKey: known ? `hosted-${user}-after-${known}` : `hosted-${user}`,\n resources: { cpuCores: policy.cpuCores, memoryMB: policy.memoryMB, diskGB: policy.diskGB },\n backend, secrets: [], sshEnabled: false,\n egressPolicy: { mode: 'strict', allowDomains: policy.allowDomains, includeImplicitDomains: false },\n idleTimeoutSeconds: policy.idleTimeoutSeconds, maxLifetimeSeconds: policy.maxLifetimeSeconds,\n deleteAfterStoppedSeconds: policy.deleteAfterStoppedSeconds, metadata: { hostedUser: user },\n })\n await store.put(key, box.id)\n }\n if (RESUMABLE.has(box.status)) await box.resume()\n if (box.status === 'running') return box\n const timeoutMs = deadline - Date.now()\n if (timeoutMs < 1000) return null\n try {\n return await sandbox.waitForRunning(box.id, { timeoutMs })\n } catch (error) {\n if (error instanceof Error && error.name === 'TimeoutError') return null\n throw error\n }\n }\n\n /** Admit a new message once: STOP/START, the free allowance, then `allow`. */\n async function admit(message: HostedMessage, user: string): Promise<Allowance> {\n const stopKey = `stop:${user}`\n if (message.channel !== 'voice' && STOP.test(message.text.trim())) { await store.put(stopKey, '1'); return { reply: NOTICE.stopped } }\n if (message.channel !== 'voice' && START.test(message.text.trim())) { await store.put(stopKey, ''); return { reply: NOTICE.started } }\n if (await store.get(stopKey)) return 'ignore'\n const dayKey = `turns:${user}:${new Date().toISOString().slice(0, 10)}`\n // KV has no atomic increment, so concurrent messages may overshoot the\n // allowance by a few; the sponsor key's budget is the hard ceiling.\n const used = Number(await store.get(dayKey) ?? 0)\n const decision = used < (config.freeTurnsPerDay ?? 20) ? 'answer'\n : config.allow ? await config.allow(message, used) : { reply: NOTICE.limit }\n if (decision === 'answer') await store.put(dayKey, String(used + 1), { expirationTtl: TWO_DAYS })\n return decision\n }\n\n /** Run one message in the person's box until it answers or `deadline`. */\n async function ask(message: HostedMessage, options: { deadline: number }): Promise<AskResult> {\n const text = message.text.trim()\n if (!text || text.length > 8000 || !/^[\\w-]{1,120}$/.test(message.turnId)) return { state: 'declined' }\n const user = (await sha256(message.userId)).slice(0, 32)\n // Scoped to the person: a voice ticket is model-supplied, so a turn id\n // admitted for one person must not skip another person's allowance.\n const admittedKey = `turn:${user}:${message.turnId}`\n if (!await store.get(admittedKey)) {\n const decision = await admit(message, user)\n if (decision === 'ignore') return { state: 'declined' }\n if (decision !== 'answer') return { state: 'declined', reply: decision.reply }\n await store.put(admittedKey, '1', { expirationTtl: TWO_DAYS })\n }\n const box = await ensureBox(user, options.deadline)\n if (!box) return { state: 'pending' }\n // A session binds its backend when created, and the runtime would give a\n // bare session its generic default assistant. A new profile starts a new\n // conversation, so the persona always matches the one in force.\n const sessionId = `hosted-${user}-${await profileTag}`\n if (!await box.session(sessionId).status()) await box.createSession({ sessionId, retention: 'workspace', backend })\n const prompt = message.channel === 'voice' ? `[Phone call. Answer in one to three short spoken sentences.]\\n${text}` : text\n for (;;) {\n const result = await box.driveConversationTurn(prompt, { sessionId, turnId: message.turnId, wallCapMs, timeoutMs: 8000 })\n if (result.state === 'completed') {\n const answer = result.text.trim()\n if (!answer) throw new HostedAgentError('empty_reply', 'The turn completed with no reply text.')\n return { state: 'answered', text: answer }\n }\n if (result.state === 'failed') throw new HostedAgentError('turn_failed', result.error)\n if (Date.now() + 2000 > options.deadline) return { state: 'pending' }\n await sleep(2000)\n }\n }\n\n return {\n ask,\n\n /**\n * One-time setup: route an Inkbox iMessage identity's messages, connected\n * to Hub under the developer's account, to `callbackUrl`, and allow the\n * one reply action on that connection. Safe to repeat.\n */\n async connect(input: { connectionId: string; identityId: string; callbackUrl: string; secret: string }) {\n const webhook = await hub.connections.inboundWebhook.get(input.connectionId)\n if (!webhook.configured) await hub.connections.inboundWebhook.create(input.connectionId, { identityId: input.identityId, imessage: true })\n await hub.permissions.set({ connectionId: input.connectionId, actionPath: IMESSAGE_REPLY, decision: 'allow' })\n const { subscription } = await hub.eventSubscriptions.create({\n clientReference: `hosted-agent:${input.connectionId}`, label: 'Hosted agent messages',\n source: { type: 'connection', connectionId: input.connectionId }, event: IMESSAGE_EVENT,\n callback: { url: input.callbackUrl, secret: input.secret },\n })\n return subscription\n },\n\n /** The Hub callback. Authenticates within Hub's 10 s budget and runs nothing. */\n async receive(request: Request, secret: string): Promise<{ response: Response; inbound?: HostedInbound }> {\n const authenticated = await authenticateHubEventRequest({ request, secret, maxBodyBytes: 64 * 1024 })\n if (!authenticated.ok) return { response: authenticated.response }\n const { runId, source, providerEvent } = authenticated.delivery\n const ack = new Response(null, { status: 204 })\n if (source.kind !== 'connection' || source.event !== IMESSAGE_EVENT || providerEvent.connectionId !== source.id) return { response: ack }\n return { response: ack, inbound: { runId, connectionId: source.id, event: providerEvent } }\n },\n\n /**\n * Answer one inbound message and send the reply through Hub. Returns\n * 'pending' while the turn still runs; call again with the same inbound.\n * On `lastAttempt`, a failure is answered with an apology instead of thrown.\n */\n async respond(inbound: HostedInbound, options: { lastAttempt?: boolean } = {}): Promise<'replied' | 'ignored' | 'pending'> {\n const normalized = normalizeConversationEvent(inbound.event)\n if (!normalized.ok) return 'ignored'\n const event = normalized.event\n const userId = normalizeAddress(event.sender.id ?? '')\n if (!userId || !event.text?.trim() || event.isGroup || event.historyOnly) return 'ignored'\n const turnId = `t-${(await sha256(inbound.runId)).slice(0, 40)}`\n let text: string\n try {\n const result = await ask({ userId, channel: 'imessage', text: event.text, turnId },\n { deadline: Date.now() + wallCapMs + 60_000 })\n if (result.state === 'pending') return 'pending'\n if (result.state === 'declined' && !result.reply) return 'ignored'\n text = result.state === 'answered' ? result.text : result.reply!\n } catch (error) {\n // A failed turn fails the same way on every retry; anything else may clear.\n if (!(error instanceof HostedAgentError) && !options.lastAttempt) throw error\n console.error(`[hosted-agent] turn=${turnId} ${describe(error)}`)\n text = NOTICE.unavailable\n }\n const plan = buildMessagingReply(inbound.event, text.slice(0, 1500), `reply-${turnId}`)\n if (!plan.ok) return 'ignored'\n // Hub replays the first result for a retry with this key and input.\n await hub.tools.invoke(plan.reply.action, plan.reply.input, { connectionId: inbound.connectionId, idempotencyKey: plan.reply.idempotencyKey })\n return 'replied'\n },\n\n /**\n * ph0ny's call hook. `admit` returns a call token bound to the caller's\n * number from the carrier's signed call record, never from the model.\n */\n async voiceHook(request: Request): Promise<Response> {\n const secret = config.voiceSecret\n if (!secret || !await equal(request.headers.get('authorization'), `Bearer ${secret}`)) return json({ error: 'unauthorized' }, 401)\n const body = await request.json().catch(() => null) as { event?: string; phone?: string } | null\n if (body?.event !== 'admit') return json({ ok: true })\n const phone = normalizeAddress(body.phone ?? '')\n if (!phone?.startsWith('+')) return json({ admit: false, say: 'Sorry, I can only take calls from a visible number.' })\n if (await store.get(`stop:${(await sha256(phone)).slice(0, 32)}`)) return json({ admit: false, say: 'You unsubscribed from this line.' })\n const claims = b64(JSON.stringify({ phone, exp: Date.now() + 2 * 3_600_000 }))\n return json({ admit: true, callToken: `hvt.${claims}.${await hmac(secret, claims)}` })\n },\n\n /**\n * ph0ny's `ask_workspace` webhook tool, configured with `forwardCallToken`.\n * A slow turn returns a ticket; the voice agent asks again with it.\n */\n async voiceAsk(request: Request): Promise<Response> {\n const secret = config.voiceSecret\n if (!secret || !await equal(request.headers.get('authorization'), `Bearer ${secret}`)) return json({ error: 'unauthorized' }, 401)\n const [tag, claims, signature] = (request.headers.get('x-voice-call-token') ?? '').split('.')\n if (tag !== 'hvt' || !claims || !await equal(signature, await hmac(secret, claims))) return json({ status: 'error', error: 'call_not_admitted' }, 403)\n const call = JSON.parse(unb64(claims)) as { phone: string; exp: number }\n if (call.exp < Date.now()) return json({ status: 'error', error: 'call_expired' }, 403)\n const body = await request.json().catch(() => null) as { utterance?: string; ticket?: string } | null\n // A ticket carries its own turn id and question, so a retry after a\n // slow first step still admits and settles exactly one turn.\n const [turnId, asked] = body?.ticket ? body.ticket.split('.') : [`v-${crypto.randomUUID()}`, b64(body?.utterance ?? '')]\n if (!turnId || !asked) return json({ status: 'error', error: 'missing_utterance' }, 400)\n try {\n const result = await ask({ userId: call.phone, channel: 'voice', text: unb64(asked), turnId }, { deadline: Date.now() + (config.voiceBudgetMs ?? 8000) })\n if (result.state === 'pending') return json({ status: 'pending', ticket: `${turnId}.${asked}` })\n if (result.state === 'declined') return json({ status: 'complete', answer: result.reply ?? 'I cannot answer that right now.' })\n return json({ status: 'complete', answer: result.text })\n } catch (error) {\n console.error(`[hosted-agent] voice turn=${turnId} ${describe(error)}`)\n return json({ status: 'error', error: 'agent_unavailable' }, 502)\n }\n },\n }\n}\n\nexport type HostedAgent = ReturnType<typeof createHostedAgent>\n"],"mappings":"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","names":[]}
|
package/dist/platform/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MembersPanel
|
|
3
|
+
} from "../chunk-GRDLGQ6A.js";
|
|
1
4
|
import {
|
|
2
5
|
InvitationsPanel
|
|
3
6
|
} from "../chunk-5MI2SKQH.js";
|
|
7
|
+
import "../chunk-AULM4X7U.js";
|
|
4
8
|
import {
|
|
5
9
|
InviteAcceptPage
|
|
6
10
|
} from "../chunk-W7P5KV6O.js";
|
|
7
|
-
import {
|
|
8
|
-
MembersPanel
|
|
9
|
-
} from "../chunk-GRDLGQ6A.js";
|
|
10
|
-
import "../chunk-AULM4X7U.js";
|
|
11
11
|
import "../chunk-6XIAPIW6.js";
|
|
12
12
|
export {
|
|
13
13
|
InvitationsPanel,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-app",
|
|
3
|
-
"version": "0.49.
|
|
3
|
+
"version": "0.49.5",
|
|
4
4
|
"packageManager": "pnpm@11.24.0",
|
|
5
5
|
"description": "Build agent applications with typed chat, tools, sandboxes, integrations, billing, and evaluation.",
|
|
6
6
|
"keywords": [
|
|
@@ -212,6 +212,11 @@
|
|
|
212
212
|
"import": "./dist/integrations/index.js",
|
|
213
213
|
"default": "./dist/integrations/index.js"
|
|
214
214
|
},
|
|
215
|
+
"./hosted-agent": {
|
|
216
|
+
"types": "./dist/hosted-agent/index.d.ts",
|
|
217
|
+
"import": "./dist/hosted-agent/index.js",
|
|
218
|
+
"default": "./dist/hosted-agent/index.js"
|
|
219
|
+
},
|
|
215
220
|
"./interactions": {
|
|
216
221
|
"types": "./dist/interactions/index.d.ts",
|
|
217
222
|
"import": "./dist/interactions/index.js",
|
|
@@ -543,6 +548,7 @@
|
|
|
543
548
|
"@tangle-network/agent-profile-materialize": "0.20.1",
|
|
544
549
|
"@tangle-network/agent-runtime": "0.249.1",
|
|
545
550
|
"@tangle-network/brand": "1.5.0",
|
|
551
|
+
"@tangle-network/hub-sdk": "0.14.2",
|
|
546
552
|
"@tangle-network/sandbox": "0.45.0",
|
|
547
553
|
"@tangle-network/sandbox-ui": "0.113.3",
|
|
548
554
|
"@tangle-network/ui": "11.9.0",
|
|
@@ -603,6 +609,7 @@
|
|
|
603
609
|
"@tangle-network/agent-profile-materialize": ">=0.20.1 <0.21.0",
|
|
604
610
|
"@tangle-network/agent-runtime": ">=0.249.1 <0.250.0",
|
|
605
611
|
"@tangle-network/brand": ">=1.5.0",
|
|
612
|
+
"@tangle-network/hub-sdk": ">=0.12.6 <0.15.0",
|
|
606
613
|
"@tangle-network/sandbox": ">=0.45.0 <0.46.0",
|
|
607
614
|
"@tangle-network/sandbox-ui": ">=0.113.3 <0.114.0",
|
|
608
615
|
"@tangle-network/ui": ">=11.6.0 <12.0.0",
|
|
@@ -702,6 +709,9 @@
|
|
|
702
709
|
},
|
|
703
710
|
"react-dom": {
|
|
704
711
|
"optional": true
|
|
712
|
+
},
|
|
713
|
+
"@tangle-network/hub-sdk": {
|
|
714
|
+
"optional": true
|
|
705
715
|
}
|
|
706
716
|
},
|
|
707
717
|
"dependencies": {
|