Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .node-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
1 change: 1 addition & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default defineConfig({
}),
starlight({
title: 'AutoMem Docs',
disable404Route: true,
logo: {
src: './src/assets/robot-icon.svg',
replacesTitle: false,
Expand Down
20 changes: 20 additions & 0 deletions functions/api/v1/customer-portal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createCustomerPortalSession } from '../../lib/managed-cloud/providers.js';
import { findAccountByToken, recordFunnelEvent } from '../../lib/managed-cloud/store.js';
import { jsonResponse } from '../../lib/managed-cloud/utils.js';

export async function onRequestPost({ request, env, params }) {
const account = await findAccountByToken(env, params.token);
if (!account) {
return jsonResponse({ success: false, error: 'Account not found.' }, { status: 404 });
}
if (!account.stripe_customer_id) {
return jsonResponse({ success: false, error: 'Stripe customer is not configured yet.' }, { status: 400 });
}
const session = await createCustomerPortalSession(env, request, account);
await recordFunnelEvent(env, {
token: params.token,
event_name: 'customer_portal_opened',
page: '/dashboard',
});
return jsonResponse({ success: true, url: session.url });
}
32 changes: 32 additions & 0 deletions functions/api/v1/enrich.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { runBraveEnrichment } from '../../lib/managed-cloud/providers.js';
import { findAccountByToken, getEnrichmentItems, getOnboardingSession, recordFunnelEvent, saveEnrichmentItems, updateEnrichmentSelections, upsertOnboardingSession } from '../../lib/managed-cloud/store.js';
import { jsonResponse, readJson } from '../../lib/managed-cloud/utils.js';

export async function onRequestPost({ request, env, params }) {
const account = await findAccountByToken(env, params.token);
if (!account) {
return jsonResponse({ success: false, error: 'Account not found.' }, { status: 404 });
}
const body = await readJson(request);

if (Array.isArray(body.items)) {
const items = await updateEnrichmentSelections(env, params.token, body.items);
return jsonResponse({ success: true, items });
}

const session = await getOnboardingSession(env, params.token);
const identity = body.identity || session?.profile?.identity || {};
if (!identity?.raw && !identity?.name) {
return jsonResponse({ success: false, error: 'Identity details are required before enrichment can run.' }, { status: 400 });
}
const items = await runBraveEnrichment(env, identity);
const saved = await saveEnrichmentItems(env, params.token, items);
await upsertOnboardingSession(env, params.token, { enrichment_requested: true });
await recordFunnelEvent(env, {
token: params.token,
event_name: 'enrichment_requested',
page: '/onboarding',
properties: { result_count: saved.length },
});
return jsonResponse({ success: true, items: saved });
}
79 changes: 79 additions & 0 deletions functions/api/v1/onboarding.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { advanceInterview, buildInterviewState, createInitialInterviewMessage, skipInterview } from '../../lib/managed-cloud/interview.js';
import { generateInterviewReply } from '../../lib/managed-cloud/providers.js';
import { findAccountByToken, getEnrichmentItems, getOnboardingSession, recordFunnelEvent, upsertOnboardingSession } from '../../lib/managed-cloud/store.js';
import { jsonResponse, readJson } from '../../lib/managed-cloud/utils.js';

export async function onRequestGet({ env, params }) {
const account = await findAccountByToken(env, params.token);
if (!account) {
return jsonResponse({ success: false, error: 'Account not found.' }, { status: 404 });
}
let session = await getOnboardingSession(env, params.token);
if (!session) {
session = await upsertOnboardingSession(env, params.token, {});
}
if (!session.transcript?.length) {
session = await upsertOnboardingSession(env, params.token, {
transcript: [{ role: 'assistant', content: createInitialInterviewMessage() }],
});
}
return jsonResponse({
success: true,
onboarding: buildInterviewState(session),
enrichment_items: await getEnrichmentItems(env, params.token),
});
}

export async function onRequestPost({ request, env, params }) {
const account = await findAccountByToken(env, params.token);
if (!account) {
return jsonResponse({ success: false, error: 'Account not found.' }, { status: 404 });
}
const body = await readJson(request);
const session = await getOnboardingSession(env, params.token) || await upsertOnboardingSession(env, params.token, {});

if (body.skip) {
const next = await upsertOnboardingSession(env, params.token, skipInterview(session));
await recordFunnelEvent(env, {
token: params.token,
event_name: 'onboarding_skipped',
page: '/onboarding',
});
return jsonResponse({
success: true,
onboarding: buildInterviewState(next),
reply: next.transcript.at(-1)?.content || '',
});
}

if (!body.message) {
return jsonResponse({
success: true,
onboarding: buildInterviewState(session),
reply: session.transcript?.at(-1)?.content || createInitialInterviewMessage(),
});
}

const assistantReply = await generateInterviewReply(env, session, body.message).catch(() => null);
const patch = await advanceInterview({
session,
userMessage: body.message,
assistantReply,
});
const next = await upsertOnboardingSession(env, params.token, patch);
await recordFunnelEvent(env, {
token: params.token,
event_name: 'onboarding_answered',
page: '/onboarding',
properties: {
current_question_index: next.current_question_index,
complete: next.interview_complete,
},
});

return jsonResponse({
success: true,
onboarding: buildInterviewState(next),
reply: patch.reply,
});
}
46 changes: 46 additions & 0 deletions functions/api/v1/preseed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { buildPreseedPayload } from '../../lib/managed-cloud/preseed.js';
import { findAccountByToken, getEnrichmentItems, getOnboardingSession, recordFunnelEvent, upsertOnboardingSession } from '../../lib/managed-cloud/store.js';
import { jsonResponse, nowIso } from '../../lib/managed-cloud/utils.js';
import { submitPreseedPayload } from '../../lib/managed-cloud/providers.js';

export async function onRequestPost({ env, params }) {
const account = await findAccountByToken(env, params.token);
if (!account) {
return jsonResponse({ success: false, error: 'Account not found.' }, { status: 404 });
}
const session = await getOnboardingSession(env, params.token);
if (!session) {
return jsonResponse({ success: false, error: 'Onboarding session not found.' }, { status: 404 });
}
if (session.preseeded_at) {
return jsonResponse({
success: true,
...session.preseed_summary,
preseeded_at: session.preseeded_at,
});
}

const enrichmentItems = await getEnrichmentItems(env, params.token);
const payload = buildPreseedPayload(session.profile || {}, enrichmentItems);
const result = await submitPreseedPayload(env, account, payload);
const preseeded_at = nowIso();
await upsertOnboardingSession(env, params.token, {
preseeded_at,
preseed_summary: {
...result,
preseeded_at,
},
});
await recordFunnelEvent(env, {
token: params.token,
event_name: 'preseed_completed',
page: '/onboarding',
properties: result,
});

return jsonResponse({
success: true,
...result,
preseeded_at,
});
}
134 changes: 134 additions & 0 deletions functions/api/v1/signup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { DEFAULT_TRIAL_DAYS } from '../../lib/managed-cloud/constants.js';
import { createInstapodsPod, createStripeCustomer, sendManagedCloudVerificationEmail } from '../../lib/managed-cloud/providers.js';
import { createDefaultOnboardingSession, findAccountByEmail, insertAccount, putIdempotencyRecord, getIdempotencyRecord, recordFunnelEvent } from '../../lib/managed-cloud/store.js';
import { buildFeaturesForTier, buildAbsoluteUrl, addDays, generateManagedToken, isValidEmail, jsonResponse, normalizeEmail, nowIso, readJson } from '../../lib/managed-cloud/utils.js';

async function verifyTurnstile(request, env, turnstileToken) {
if (!env?.TURNSTILE_SECRET_KEY) {
return true;
}
if (!turnstileToken) {
return false;
}
const form = new URLSearchParams();
form.set('secret', env.TURNSTILE_SECRET_KEY);
form.set('response', turnstileToken);
const ip = request.headers.get('CF-Connecting-IP');
if (ip) {
form.set('remoteip', ip);
}
const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
body: form,
});
const payload = await response.json();
return Boolean(payload.success);
}

export async function onRequestPost({ request, env, waitUntil }) {
const body = await readJson(request);
const url = new URL(request.url);
const email = normalizeEmail(body.email || url.searchParams.get('email'));
const name = String(body.name || url.searchParams.get('name') || '').trim();
const source = String(body.source || url.searchParams.get('source') || '/start');

if (!await verifyTurnstile(request, env, body.turnstileToken || url.searchParams.get('turnstileToken'))) {
return jsonResponse({ success: false, error: 'Verification failed.' }, { status: 400 });
}

if (!isValidEmail(email)) {
return jsonResponse({ success: false, error: 'Enter a valid email address.' }, { status: 400 });
}

const idempotencyKey = request.headers.get('x-idempotency-key');
if (idempotencyKey) {
const existingRecord = await getIdempotencyRecord(env, idempotencyKey);
if (existingRecord?.response) {
return jsonResponse(existingRecord.response);
}
}

const existing = await findAccountByEmail(env, email);
if (existing) {
const response = {
success: true,
token: existing.token,
status: existing.status,
requires_email_verification: !existing.email_verified_at,
onboarding_url: buildAbsoluteUrl(request, env, '/onboarding', { token: existing.token }),
trial_days_remaining: DEFAULT_TRIAL_DAYS,
};
await recordFunnelEvent(env, {
token: existing.token,
event_name: 'signup_revisit',
page: source,
properties: { email },
});
return jsonResponse(response);
}

const created_at = nowIso();
const token = generateManagedToken();
const stripeCustomer = await createStripeCustomer(env, { email, name });

const accountDraft = {
token,
email,
name,
email_verified_at: null,
stripe_customer_id: stripeCustomer.id,
stripe_subscription_id: null,
instapods_pod_id: null,
pod_url: null,
mcp_url: null,
tier: 'starter',
status: 'provisioning',
connected_agents: [],
features: buildFeaturesForTier('starter'),
trial_start: created_at,
trial_end: addDays(created_at, DEFAULT_TRIAL_DAYS),
created_at,
updated_at: created_at,
};

const instapodsPod = await createInstapodsPod(env, accountDraft);
const account = await insertAccount(env, {
...accountDraft,
instapods_pod_id: instapodsPod.pod_id,
pod_url: instapodsPod.url,
mcp_url: instapodsPod.mcp_url,
status: instapodsPod.status || 'provisioning',
});
await createDefaultOnboardingSession(env, token);
await recordFunnelEvent(env, {
token,
event_name: 'signup_created',
page: source,
properties: {
has_name: Boolean(name),
},
});

if (typeof waitUntil === 'function') {
waitUntil(sendManagedCloudVerificationEmail(env, request, account).catch(() => null));
} else {
await sendManagedCloudVerificationEmail(env, request, account).catch(() => null);
}

const response = {
success: true,
token,
status: account.status,
requires_email_verification: true,
onboarding_url: buildAbsoluteUrl(request, env, '/onboarding', { token }),
trial_days_remaining: DEFAULT_TRIAL_DAYS,
};
if (idempotencyKey) {
await putIdempotencyRecord(env, {
idempotency_key: idempotencyKey,
scope: 'signup',
response,
});
}
return jsonResponse(response);
}
41 changes: 41 additions & 0 deletions functions/api/v1/status.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { fetchPodStats, refreshProvisioningStatus } from '../../lib/managed-cloud/providers.js';
import { findAccountByToken, getOnboardingSession } from '../../lib/managed-cloud/store.js';
import { buildManualConfigSnippet, buildInstallCommand, daysRemaining, jsonResponse } from '../../lib/managed-cloud/utils.js';

export async function onRequestGet({ env, params }) {
const account = await findAccountByToken(env, params.token);
if (!account) {
return jsonResponse({ success: false, error: 'Account not found.' }, { status: 404 });
}
const refreshed = await refreshProvisioningStatus(env, account);
const onboarding = await getOnboardingSession(env, refreshed.token);
const stats = await fetchPodStats(env, refreshed);

return jsonResponse({
success: true,
token: refreshed.token,
status: refreshed.status,
pod_url: refreshed.pod_url,
mcp_url: refreshed.mcp_url,
tier: refreshed.tier,
email_verified: Boolean(refreshed.email_verified_at),
connected_agents: refreshed.connected_agents || [],
trial_days_remaining: daysRemaining(refreshed.trial_end),
features: refreshed.features || {},
stats: {
memories_stored: stats?.memories_stored || 0,
associations: stats?.associations || 0,
last_activity: stats?.last_activity || onboarding?.preseeded_at || null,
},
onboarding: {
interview_complete: Boolean(onboarding?.interview_complete),
skipped: Boolean(onboarding?.skipped),
preseeded_at: onboarding?.preseeded_at || null,
preseed_summary: onboarding?.preseed_summary || {},
},
install_command: buildInstallCommand(refreshed.token),
manual_config: refreshed.mcp_url
? buildManualConfigSnippet(refreshed.mcp_url, refreshed.token)
: null,
});
}
23 changes: 23 additions & 0 deletions functions/api/v1/subscribe.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createCheckoutSession } from '../../lib/managed-cloud/providers.js';
import { findAccountByToken, recordFunnelEvent } from '../../lib/managed-cloud/store.js';
import { jsonResponse, readJson } from '../../lib/managed-cloud/utils.js';

export async function onRequestPost({ request, env, params }) {
const account = await findAccountByToken(env, params.token);
if (!account) {
return jsonResponse({ success: false, error: 'Account not found.' }, { status: 404 });
}
const body = await readJson(request);
const plan = String(body.plan || '').toLowerCase();
if (!['pro', 'ultimate'].includes(plan)) {
return jsonResponse({ success: false, error: 'Choose a valid plan.' }, { status: 400 });
}
const session = await createCheckoutSession(env, request, account, plan);
await recordFunnelEvent(env, {
token: params.token,
event_name: 'checkout_started',
page: '/subscribe',
properties: { plan },
});
return jsonResponse({ success: true, url: session.url });
}
Loading