Repository navigation
Expand file tree
/
Copy pathindex.js
More file actions
2620 lines (2569 loc) · 253 KB
/
Copy pathindex.js
File metadata and controls
2620 lines (2569 loc) · 253 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// 1Helm Cloud prototype (Phase A, workers.dev, Stripe TEST mode). Path routing: /w/<slug>.
import { clientLog } from "./internal-shim.js";
import { Ledger } from "./ee/index.js";
import { UsageLedger } from "./usage-ledger.js";
import { installSnapshotRpc } from "./sqlite-snapshot.js";
import { backupControl, isBackupPath } from "./backup-control.js";
import { enabled as eeEnabled, features as eeFeatures, routeEnabled as eeRouteEnabled } from "./ee/index.js";
import { tolerantEnv, DO_TRANSIENT_RE, RPC_SKEW_RE, rpcSkew, codeV, isResetErr } from "./do-stub.js";
import { opsRoute, viewAsGate, viewAsToken } from "./internal-shim.js";
import { ambientSettingsRoute } from "./ee/index.js";
import { SW_RE, swResponse } from "./pwa.js"; // Web Push service worker (notify.js)
import { notifApi } from "./notify.js";
import { meetingsApi, meetingsCron } from "./ee/index.js"; // Meeting reminders for notes (Google Calendar / Outlook → "Take notes?")
import { reportApi, reportsPage, reportShot } from "./reports.js";
import { MANIFEST_RE, PWA_ICON_RE, manifestResponse, pwaHead, iconName } from "./pwa.js";
import { cardRoute } from "./cards.js";
import { logoRoute } from "./logo.js";
import { appLogoRoute } from "./ee/index.js";
import { isPublicShare, servePublic, shareApi, attachOffers } from "./share.js";
import { safeName, mimeOf, servedHeaders, MAX_UPLOAD, MAX_VIEW, storeUpload, storeView } from "./files.js";
import { previewFileName } from "./file-preview.js";
import { stComplete, stDelete, room, fullMessage, noteRefused, storageOf, storageView, setBlocks, syncBlocks, recount, recountStale, noticeSeen, rearmNotice, RECOUNT_MS } from "./storage.js";
import { readImportManifest, readImportRows, validateImportManifest } from "./import-data.js";
import { verifyImportLayer } from "./import-tar.js";
import { boundedText, boundedJson, boundedForm, BodyTooLargeError, UnsupportedFormTypeError } from "./bounded-body.js";
import { appDetail } from "./ee/index.js";
import { transportFor } from "./ee/index.js"; // the Composio transport: connections, connect links, disconnect
import { composioWebhook } from "./ee/index.js"; // stage 5: signed app events → the workflow that owns the trigger
import { appsMode, selfStatus, selfAvailable, signInStart, signInFinish, isEnded, appsModeRoute, saveOnboardingChoice, composioDonePage } from "./ee/index.js"; // Managed vs Self-managed apps (part 2)
import { paypalSetupRoute } from "./ee/index.js";
import { createChannel, insertChannel, ChannelError } from "./channels.js";
import { transcribe } from "./transcribe.js";
import { lnAudio } from "./ee/index.js"; // Live notes from the device's microphone: one chunk → speech to text (transcribe.js) → the LiveNotes DO
import { transcribeLive, ledgerDictation } from "./transcribe-live.js";
import { DurableObject, WorkerEntrypoint } from "cloudflare:workers";
import { deletionRoute, deletionCron, deleteNote } from "./deletion.js";
import { archiveDeletionMix, wipeMix } from "./deletion-do.js";
import { pomoGlobalMethods } from "./pomodoro.js";
export { ChannelComputer, ComputerLink } from "./channel-computer.js";
export { NativeTransport, NativeTransportState } from "./native-transport.js";
export { Resident } from "./resident.js";
export { UsageLedger };
export { Sentinel } from "./sentinel.js";
import { sealSecret, newStandin } from "./sentinel.js";
import { linkStartConnect, linkStatus, linkDisconnect, linkListPurchases, linkGetPurchase, linkApi } from "./ee/index.js";
import { beeRoute, beeJobOn, beeJobAlarm, beeLiveOn, beeLiveStopOn, beeOps, beeCron, beePendingFacts, beeNoteEdited, beeNoteDeleted, beeNoteRestored } from "./ee/index.js"; // Settings → Ambient → Bee: pairing, sealed token, /v1/me in a container job
export { BeeSync } from "./ee/index.js";
export { LiveNotes } from "./ee/index.js";
export { FilePrep } from "./sidecar.js"; // file sidecars: one object per file version prepares its text in the background
import { prepJobOn, prepJobAlarm } from "./sidecar.js";
import { lnName } from "./ee/index.js"; // Bee Live notes: one DO per task (SPEC §9): the polling job, the note-taker, the final pass, caps // Bee live sync: one DO per person, a 5-min alarm, conversations → General's memory + R2 transcripts
import { listLogins, saveLogin, deleteLogin, importLogins, loginNote } from "./logins.js";
import { accountRoute, shortcutsView, emailChangeRoute, newSession, endSession } from "./account.js";
import { ownerAccount, ownerAllowsEmail } from "./single-owner.js";
import { landingPage, selfHostedSignInPage, checkEmailPage, expiredPage, onboardPage, textingPage, hostSignInPage } from "./onboarding.js";
import { createSignIn, decoyPending, verifyCode, pendingEmail, pendingCookie, clearPendingCookie, signInEmail, sweepCodeFails, RESEND_WAIT } from "./signin-code.js";
import { releasesPage, pushRelease, undoRelease, isReleaseAdmin } from "./internal-shim.js";
import { kanbanProxy } from "./internal-shim.js";
import { AGE_VERSION, AGE_ERROR, ageOf, ageCookie, agePage, ageNext, recordAge } from "./age-gate.js";
import { modelsRoute } from "./ee/index.js";
import { hindsightSelfTest, hsOn, sweepBanks, recall as hsRecall, bankId as hsBankId } from "./hindsight.js";
import { saveNightlyPool } from "./ee/index.js";
import { route2On } from "./ee/index.js";
import { privacyBoot, privacyView, setPrivacy, isPrivacyLevel } from "./privacy.js"; // AI data privacy (onboarding step, boot)
import { routerPage, routerData, routerLabApi } from "./internal-shim.js";
import { pruneShadow, shadowData } from "./ee/index.js";
import { decisionCaller, askDecider, jobModel, jobRoute } from "./ee/index.js";
import { pruneLab, labData } from "./internal-shim.js";
import { parityRoute } from "./internal-shim.js";
import { pageDialog } from "./page-dialog.js";
import { helperName, DEFAULT_HELPER } from "./helper-name.js";
import { interviewEnsure, interviewApi, bootView as interviewBoot, rowOf as interviewRow } from "./interview.js";
import { interviewChatApi } from "./interview-chat.js";
import { appsUsedApi } from "./ee/index.js";
import { googleStatus, googleDisconnect, listConnections, setDefaultAccount, setAccountNote, tidyAfterConnect, appName, disconnectAllManaged, managedCount, RETIRED_APPS, RETIRED_NOTE } from "./ee/index.js";
import { connectOfferApi, connectOfferDone, offerCallbackQuery, catalogFile } from "./ee/index.js";
import { cancelApi, cancelWebhook, cancelSync, cancelCron } from "./ee/index.js";
import { holdOf, holdView, holdFromFailure, notePaid, pastdueSync, pastdueCron, pastdueApi, pastdueSeen } from "./ee/index.js";
import { PAY_MSG, INACTIVE_MSG } from "./ee/index.js"; // payment failed: read-only now, deleted after the paid period + 7 days
import { changePlan } from "./ee/index.js";
import { isTestAccount, usd as guardUsd } from "./spendguard.js";
import { ledgerGuard } from "./ee/index.js"; // paid-plan Ledger accounting and notices
import { textingStatus, textingStart, textingPoll, textingNewCode, textingOff, textingWebhook, textingUseProject, textingDebug } from "./ee/index.js";
import { customMcpRoute } from "./ee/index.js";
const NANO = 1e9; // balances in nano-dollars (integers)
const PRICE = { in: 150, cached: 30, out: 500 }; // glm-5.3-flash nano$/token (CF list); Workers AI fallback only
import { useOpenRouter, openRouterChat, OR_MAX_OUT, OR_RESERVE_PRICE, aiOpts } from "./model.js";
import { byokApi, loadByok, byokChat, BYOK_PLAN, ledgerByok } from "./byok.js";
import { orStart, orFinish, orCookie, orCookieOf } from "./byok-openrouter.js"; // Sign in with OpenRouter (the BYOK panel's optional button) // own AI key: the byok plan's settings + routing (+ a backup key on a full plan)
import { People } from "./people.js";
import { memjobsCron, REFLECT_ID, REFLECT_MAX } from "./memjobs.js";
import { permsApi } from "./perms.js"; // Settings → Permissions: level + "Always allow" kinds and sites
import { nativeSameOrigin, SSH_PERMS } from './native-network.js';
import { transportGate, transportHeaders, httpsOrigin } from './transport-security.js';
import { importContacts, photoOf, IMPORT_BATCH_MAX } from "./contacts-import.js";
import { scrubSecrets, hasSecret, SECRET_NOTE } from "./secrets-scrub.js"; // nosecrets: no password, key, card number or code in the Notebook
import { locationRoute, saveLocation, cleanFix, pickHome } from "./location.js"; // where the person is: the app's coarse fix, the helper's place line
import { replyRef } from "./reply-to.js"; // Reply on a chat bubble: the quoted message rides with /stream and send-later
const MARKUP = 1.15;
const MAX_OUT = 2048;
const RESERVED = new Set("admin api app assets auth billing blog cdn demo docs help mail provision security status support www 1helm cloudflare login signup static dev staging test internal captain root system cloud gateway portal account accounts dashboard console design models hooks onboard download downloads manual terms privacy legal pricing about home news careers jobs press email smtp imap ftp ns ns1 ns2 m mobile ios android".split(" "));
const slugValid = (s) => /^(?=.{3,48}$)[a-z0-9](?:[a-z0-9-]*[a-z0-9])$/.test(s) && !s.includes("--") && !/^\d+$/.test(s) && !RESERVED.has(s);
const enc = new TextEncoder();
const hex = (buf) => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
const sha256 = async (s) => hex(await crypto.subtle.digest("SHA-256", enc.encode(s)));
const rand = (n = 32) => hex(crypto.getRandomValues(new Uint8Array(n)));
const now = () => Date.now();
const json = (b, s = 200, h = {}) => new Response(JSON.stringify(b), { status: s, headers: { "content-type": "application/json", "cache-control": "no-store", ...h } });
const selfHostedPage = async (env, data = {}) => selfHostedSignInPage({ ...data, claimed: !!(await env.DB.prepare("SELECT user_id FROM single_owner WHERE id=1").first()) });
const scriptJson = (b) => JSON.stringify(b).replace(/</g, "\\u003c");
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
const usd = (nano) => "$" + (nano / NANO).toFixed(2);
function page(title, body) {
return new Response(`<!doctype html><html><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel="icon" type="image/png" href="/1helm-icon-64.png"><title>${esc(title)}</title>
<style>body{font:16px system-ui;max-width:760px;margin:40px auto;padding:0 16px;color:#111}input,button{font:inherit;padding:10px;border-radius:8px;border:1px solid #bbb}button{background:#111;color:#fff;cursor:pointer}
.pill{position:fixed;top:12px;right:12px;padding:6px 12px;border-radius:999px;background:#e8f5e9}.pill.warn{background:#fff3cd}.pill.low{background:#ffd7d7}
#log{border:1px solid #ddd;border-radius:10px;padding:12px;min-height:240px;white-space:pre-wrap}.me{color:#555}.cost{color:#888;font-size:12px}
#modal{display:none;position:fixed;inset:0;background:#0008}#modal>div{background:#fff;max-width:380px;margin:18vh auto;padding:20px;border-radius:12px}</style>${pageDialog("classic")}</head><body>${body}</body></html>`,
{ headers: { "content-type": "text/html;charset=utf-8", "cache-control": "no-store" } });
}
async function stripe(env, path, params = {}, method = "POST") {
const body = new URLSearchParams();
const add = (k, v) => { if (v !== undefined && v !== null) { if (typeof v === "object") for (const [kk, vv] of Object.entries(v)) add(`${k}[${kk}]`, vv); else body.append(k, String(v)); } };
for (const [k, v] of Object.entries(params)) add(k, v);
const r = await fetch(`https://api.stripe.com/v1/${path}`, {
method, headers: { authorization: `Bearer ${env.STRIPE_SECRET_KEY}`, "content-type": "application/x-www-form-urlencoded" },
body: method === "GET" ? undefined : body });
const d = await r.json();
if (!r.ok) throw new Error(d.error?.message || `Stripe HTTP ${r.status}`);
return d;
}
async function verifyStripe(env, raw, header) {
const parts = (header || "").split(",").map((p) => p.trim());
const times = parts.filter((p) => p.startsWith("t=")).map((p) => p.slice(2));
const signatures = parts.filter((p) => p.startsWith("v1=")).map((p) => p.slice(3));
if (times.length !== 1 || !/^\d+$/.test(times[0]) || signatures.length < 1 || signatures.length > 16) return false;
const t = times[0];
if (!Number.isSafeInteger(Number(t)) || Math.abs(now() / 1000 - Number(t)) > 300) return false;
const key = await crypto.subtle.importKey("raw", enc.encode(env.STRIPE_WEBHOOK_SECRET), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const mac = hex(await crypto.subtle.sign("HMAC", key, enc.encode(`${t}.${raw}`)));
return signatures.some((sig) => /^[a-f0-9]{64}$/i.test(sig) && (crypto.subtle.timingSafeEqual ? crypto.subtle.timingSafeEqual(enc.encode(mac), enc.encode(sig.toLowerCase())) : mac === sig.toLowerCase()));
}
async function currentUser(req, env) {
const m = (req.headers.get("cookie") || "").match(/(?:^|;\s*)hc_session=([a-f0-9]+)/);
if (!m) return null;
return env.DB.prepare("SELECT u.* FROM sessions s JOIN users u ON u.id=s.user_id WHERE s.token_hash=? AND s.expires_at>?").bind(await sha256(m[1]), now()).first();
}
const tenantOf = (env, userId) => env.DB.prepare("SELECT * FROM tenants WHERE owner_user_id=?").bind(userId).first();
const cleanName = (value, max) => String(value || "").replace(/[\u0000-\u001f\u007f]/g, "").replace(/\s+/g, " ").trim().slice(0, max);
const onboarded = (t) => Number(t?.onboarding_completed || 0) === 1;
// v1 onboarding keeps going after payment (Ways to reach → Watch it work → First task). '' = accounts made before v1.
// v1 accounts (onboard_step set) must also have picked their own address; pre-v1 accounts ("") are left alone.
const onboardDone = (t) => !t?.onboard_step || (t.onboard_step === "done" && Number(t.slug_chosen) === 1);
const ONBOARD_V = "16"; // 16: adult acknowledgement before any new onboarding details/email
// Onboarding "Ways to reach": the Google apps offered one by one (Google Super is retired). { slug: { status, email } }.
const ONBOARD_APPS = ["gmail", "googlecalendar", "googledrive"];
async function onboardApps(env, tenantId) {
const off = Object.fromEntries(ONBOARD_APPS.map((k) => [k, { status: "unavailable" }]));
if (!env.COMPOSIO_KEY) return off;
try {
const list = await transportFor(env, tenantId).listConnections(), out = {};
for (const k of ONBOARD_APPS) { const mine = list.filter((a) => a.app === k), on = mine.find((a) => a.status === "connected" && a.default) || mine.find((a) => a.status === "connected");
out[k] = on ? { status: "connected", email: on.email || "" } : mine.length ? { status: "expired" } : { status: "none" }; }
return out;
} catch { return off; }
}
// The onboarding "how should I reach your apps?" step: the current side, whether Self-managed is offered here, and the Composio sign-in.
async function onboardAppsChoice(env, t) {
const s = await selfStatus(env, t.id);
return { mode: await appsMode(env, t.id), available: selfAvailable(env), signin: s.status, who: s.who, connect_url: `/w/${t.slug}/composio/connect?back=close` };
}
const onboardNext = (t) => !t ? "/onboard" : t.status !== "active" ? null : onboarded(t) ? `/w/${t.slug}/app` : t.first_name ? "/onboard/helper" : "/onboard/profile";
const APP_V = "224";
const archive = (env, tenantId) => env.ARCHIVE.get(env.ARCHIVE.idFromName(`archive:${tenantId}`));
const IMPORT_PART = 50 * 1024 * 1024;
export async function handleImport(req, env, tenantId, importId, rest) {
const A = archive(env, tenantId);
const tok = (req.headers.get("authorization") || "").replace(/^Bearer\s+/i, "");
if (!tok || !(await A.auth(importId, await sha256(tok)))) return json({ error: "import not authorized" }, 401);
const m = rest.match(/^\/obj\/([a-z0-9._-]{1,80})(?:\/(part)\/(\d+)|\/(complete))?$/);
if (rest === "/status" && req.method === "GET") return json({ objects: await A.listObjects(importId) });
if (m && !m[2] && !m[4] && req.method === "POST") {
const name = m[1], existing = await A.object(importId, name);
if (existing) return json({ name, resumed: true, parts: JSON.parse(existing.parts).map((x) => x.partNumber) });
const key = `t/${tenantId}/import/${importId}/${name}`, mpu = await env.WS.createMultipartUpload(key, { customMetadata: { app: "1hc-proto", import: importId } });
await A.putObject(importId, name, key, mpu.uploadId); return json({ name, resumed: false, parts: [] });
}
if (m && m[2] && req.method === "PUT") {
const partNumber = Number(m[3]);
if (!Number.isSafeInteger(partNumber) || partNumber < 1 || partNumber > 10000) return json({ error: "part number must be 1 to 10000" }, 400);
const o = await A.object(importId, m[1]); if (!o) return json({ error: "start the object first" }, 404);
const rawLength = req.headers.get("content-length") || "", len = Number(rawLength);
if (!req.body || !/^[0-9]+$/.test(rawLength) || !Number.isSafeInteger(len) || len < 1 || len > IMPORT_PART) return json({ error: "part needs content-length <= 50 MB" }, 400);
const rm = await room(env, tenantId, len); if (!rm.ok) return json({ error: fullMessage(rm), storage_full: true }, 507); // storage.js
const up = await env.WS.resumeMultipartUpload(o.key, o.upload_id).uploadPart(partNumber, req.body.pipeThrough(new FixedLengthStream(len)));
const n = await A.addPart(importId, m[1], { partNumber: up.partNumber, etag: up.etag, bytes: len }); return json({ part: partNumber, received: n });
}
if (m && m[4] && req.method === "POST") {
const result = await A.completeImportObject(importId, m[1], tenantId);
return json(result.body, result.status);
}
if (rest === "/finish" && req.method === "POST") {
let manifest;
try { manifest = await readImportManifest(req); }
catch (e) { return json({ error: e.message }, e.message.includes("exceeds") ? 413 : 400); }
return json(await finishImport(env, tenantId, importId, manifest));
}
return json({ error: "unknown import route" }, 404);
}
// Verify every uploaded object against the exporter's manifest, register the file layers with the computer,
// load the database rows, and compare every count. Nothing is marked done unless all checks pass.
export async function finishImport(env, tenantId, importId, manifest) {
const invalid = validateImportManifest(manifest);
if (invalid) return { ok: false, problems: [invalid] };
const A = archive(env, tenantId), attempt = crypto.randomUUID();
if (!(await A.beginImportFinish(importId, attempt))) return { ok: false, problems: ["import finish is already running or complete"] };
try {
const objs = Object.fromEntries((await A.listObjects(importId)).map((o) => [o.name, o]));
const problems = [], layers = [];
for (const [name, want] of Object.entries(manifest.objects)) {
const o = objs[name]; if (!o || !o.done) { problems.push(`${name}: not uploaded`); continue; }
const head = await env.WS.head(o.key);
if (!head || head.size !== want.bytes) { problems.push(`${name}: size ${head?.size} != ${want.bytes}`); continue; }
if (want.kind === "files") {
// A matching R2 size cannot prove the computer layer is the tarball in
// the manifest. Verify its bytes before any rows or layers are committed.
let layer; try { layer = await env.WS.get(o.key); } catch { problems.push(`${name}: could not read file layer`); continue; }
if (!layer?.body || layer.size !== want.bytes) { problems.push(`${name}: file layer missing or changed`); continue; }
try {
await A.renewImportFinish(importId, attempt);
await verifyImportLayer(layer.body, want.sha256, () => A.renewImportFinish(importId, attempt));
} catch (e) { problems.push(`${name}: ${String(e?.message || "could not verify file layer").slice(0, 100)}`); continue; }
layers.push({ key: o.key, bytes: want.bytes, sha256: want.sha256, files: want.files });
}
}
if (problems.length) return { ok: false, problems };
// Stage rows until the whole gzip and every count have passed validation.
const data = await env.WS.get(objs["data.jsonl.gz"].key);
if (!data?.body) return { ok: false, problems: ["import data is missing"] };
let loaded;
try { loaded = await readImportRows(data.body, (batch) => A.stageImportRows(importId, attempt, batch), manifest.objects["data.jsonl.gz"].sha256); }
catch (e) { return { ok: false, problems: [String(e?.message || e).slice(0, 160)] }; }
const counts = await A.stagedCounts(importId, attempt), mismatches = [];
for (const [tbl, n] of Object.entries(manifest.rows)) if ((counts[tbl] || 0) !== n) mismatches.push(`${tbl}: ${counts[tbl] || 0} != ${n}`);
for (const tbl of Object.keys(counts)) if (!Object.hasOwn(manifest.rows, tbl)) mismatches.push(`${tbl}: unexpected table`);
if (mismatches.length) return { ok: false, rows_loaded: loaded, counts, expected_rows: manifest.rows, mismatches };
const C = env.COMPUTER.get(env.COMPUTER.idFromName(`computer:${tenantId}`));
const reg = await C.importLayers(tenantId, layers);
const report = { ok: true, rows_loaded: loaded, counts, expected_rows: manifest.rows, mismatches, layers: reg.layers, source: manifest.source, at: Date.now() };
await A.commitImportRows(importId, attempt, report);
return report;
} finally { await A.releaseImportFinish(importId, attempt); }
}
async function importedHtml(env, t) {
const A = archive(env, t.id), chans = await A.channels(), rep = await A.lastReport();
if (!chans.length) return `<p>No imported data yet.</p>`;
return `<p><a href="/w/${esc(t.slug)}/?classic=1">← back to workspace</a></p><h2>Imported from ${esc(rep?.source?.name || "self-hosted 1Helm")}</h2>
<p style="color:#666">Imported ${rep ? new Date(rep.at).toLocaleString() : ""}. Files are on the Linux computer under <code>/workspace/channels/<channel>/</code>.</p>
<table style="border-collapse:collapse;width:100%">${chans.map((c) => `<tr style="border-bottom:1px solid #eee"><td style="padding:6px"><a href="/w/${esc(t.slug)}/imported/${c.id}">#${esc(c.name)}</a></td><td>${c.messages} messages</td><td>${c.memory} memories</td><td>${c.workflows} workflows</td></tr>`).join("")}</table>`;
}
async function importedChannelHtml(env, t, cid) {
const d = await archive(env, t.id).channel(cid);
if (!d.channel) return `<p>Unknown channel.</p>`;
const who = (m) => m.author || (m.bot_id ? "resident" : "you");
return `<p><a href="/w/${esc(t.slug)}/imported">← all imported channels</a></p><h2>#${esc(d.channel.name)}</h2>
${d.memory.length ? `<details><summary>${d.memory.length} memories</summary><ul>${d.memory.map((m) => `<li>${esc(String(m.content).slice(0, 400))}</li>`).join("")}</ul></details>` : ""}
${d.workflows.length ? `<details><summary>${d.workflows.length} workflows</summary><ul>${d.workflows.map((w) => `<li><b>${esc(w.name)}</b> (${esc(w.status)}, every ${Math.round(w.interval_seconds / 3600)}h)</li>`).join("")}</ul></details>` : ""}
<p style="color:#666">Last ${d.messages.length} messages</p>
${d.messages.map((m) => `<div style="margin:10px 0;padding:8px;border-left:3px solid ${m.bot_id ? "#8ab" : "#ccc"}"><div style="color:#888;font-size:12px">${esc(who(m))} · ${new Date(m.created).toLocaleString()}${m.parent_id ? " · reply" : ""}</div><div style="white-space:pre-wrap">${esc(String(m.body || "").slice(0, 4000))}</div></div>`).join("")}`;
}
// Why a model call or computer minute was refused. The daily limit is a "skipped:" error so scheduled jobs don't count it as a failure.
const DAY_LIMIT_MSG = "skipped: daily safety limit reached";
const payError = (reason) => reason === "day_limit" ? Object.assign(new Error(DAY_LIMIT_MSG), { status: 429 }) : Object.assign(new Error("out_of_credits"), { status: 402 });
// 1HC plan ladder (Captain 09-29): every plan is its usage + a flat $10; each step up doubles the usage.
const PLANS = {
personal: { name: "Personal", price: 30, usage: 20, fit: "Everyday help" },
plus: { name: "Plus", price: 50, usage: 40, fit: "For a busy household" },
power: { name: "Power", price: 90, usage: 80, fit: "For a small business" },
max: { name: "Max", price: 170, usage: 160, fit: "For heavy, all-day use" },
// Captain 10-06: "if you want to do a BYOK plan, its $10 and comes with 100gb of storage. Full stop." AI calls go to the person's own
// OpenAI-compatible endpoint (src/byok.js); no included usage: everything else we run is paid from credits they top up, at cost + 15%.
[BYOK_PLAN]: { name: "Bring your own key", price: 10, usage: 0, fit: "Use your own AI service", byok: true },
};
const planPrices = (env) => { try { return JSON.parse(env.PLAN_PRICES || "{}"); } catch { return {}; } };
const planKey = (k) => (PLANS[k] ? k : "plus");
const planOfPrice = (env, priceId) => Object.entries(planPrices(env)).find(([, id]) => id === priceId)?.[0] || null;
const ledger = (env, tenantId) => env.LEDGER.get(env.LEDGER.idFromName(`tenant:${tenantId}`));
// Full or read-only. Captain 10-06: "If payment fails, instantly it becomes readonly until its fixed." No grace period: anything but an
// active plan is read-only (they can open the app, read, download and fix payment; nothing that costs anything runs). An unpaid bill
// adds why and the dates (pastdue.js): data kept until the end of the paid period, then 7 more days, then permanently deleted.
function accessOf(t, env, hold = null) {
if (t.status === "active") return { mode: "full", status: t.status };
const a = { mode: "read_only", status: t.status };
if (hold) Object.assign(a, holdView(hold));
else if (t.status === "past_due") Object.assign(a, { reason: "payment_failed", message: PAY_MSG });
return a;
}
// The plain refusal for anything that needs a working plan.
const payRequired = (access) => json({ error: "payment_required", message: access?.message || INACTIVE_MSG, access }, 402);
async function applySubStatus(env, subId, stripeStatus) {
const status = { active: "active", trialing: "active", past_due: "past_due", unpaid: "suspended", canceled: "suspended", incomplete_expired: "suspended", paused: "suspended" }[stripeStatus];
if (!status) return;
await env.DB.prepare(`UPDATE tenants SET status=?, past_due_since=CASE WHEN ?='past_due' THEN COALESCE(past_due_since,?) WHEN ?='active' THEN NULL ELSE past_due_since END,
sub_status=?, updated_at=? WHERE stripe_subscription=?`).bind(status, status, now(), status, stripeStatus, now(), subId).run();
}
// Where Stripe (and other outside pages) send people back: always the app, never the old classic page at /w/<slug>/.
// ?settings=usage opens Settings → Usage; the app also reads topup=1 / resubscribed=1 / plan=changed to say what happened.
const appUrl = (origin, t, q = "") => `${origin}/w/${t.slug}/app${q}`;
const USAGE = "?settings=usage";
// Every recovery path is self-serve: pay the open invoice (new card), or resubscribe if the subscription ended.
async function fixPayment(env, t, origin) {
if (t.stripe_subscription) {
const sub = await stripe(env, `subscriptions/${t.stripe_subscription}`, {}, "GET");
if (sub.status !== "canceled" && sub.status !== "incomplete_expired") {
const inv = sub.latest_invoice && await stripe(env, `invoices/${sub.latest_invoice}`, {}, "GET");
if (inv && inv.status === "open" && inv.hosted_invoice_url) return { url: inv.hosted_invoice_url, kind: "pay_invoice" };
const ps = await stripe(env, "billing_portal/sessions", { customer: t.stripe_customer, return_url: appUrl(origin, t, USAGE) });
return { url: ps.url, kind: "portal" };
}
}
const cs = await stripe(env, "checkout/sessions", { mode: "subscription", customer: t.stripe_customer || undefined, client_reference_id: t.id,
"line_items[0][price]": planPrices(env)[planKey(t.plan)], "line_items[0][quantity]": 1, success_url: appUrl(origin, t, `${USAGE}&resubscribed=1`), cancel_url: appUrl(origin, t, USAGE),
metadata: { app: "1hc-proto", tenant_id: t.id }, subscription_data: { metadata: { app: "1hc-proto", tenant_id: t.id } } });
return { url: cs.url, kind: "resubscribe" };
}
const hsSweep = (env, o = {}) => sweepBanks(env, { ...o, channelsOf: (tid) => archive(env, tid).channels() });
// Hourly: heal missed or out-of-order webhooks by asking Stripe for the truth.
async function reconcile(env) {
const rows = (await env.DB.prepare("SELECT id, stripe_subscription, status, sub_status FROM tenants WHERE stripe_subscription<>''").all()).results;
let fixed = 0;
for (const r of rows) {
const sub = await stripe(env, `subscriptions/${r.stripe_subscription}`, {}, "GET").catch(() => null);
if (!sub) continue;
if (sub.status !== r.sub_status) { await applySubStatus(env, r.stripe_subscription, sub.status); fixed++; }
// A payment failure (or a fix) whose webhook we missed: start or end the hold (pastdue.js).
await pastdueSync(env, await env.DB.prepare("SELECT * FROM tenants WHERE id=?").bind(r.id).first(), sub, {}).catch((e) => console.log("pastdue sync", r.id, e.message));
await syncBlocks(env, r.id, sub).catch((e) => console.log("storage blocks sync", r.id, e.message)); // storage.js: a missed subscription.updated
await cancelSync(env, r.id, sub, { stripe }).catch((e) => console.log("cancel sync", r.id, e.message)); // a portal cancel/renew whose webhook we missed (src/cancel.js)
}
return { checked: rows.length, fixed };
}
// Watchdog: any resident whose schedule is overdue by >2 min gets poked (re-arms its alarm; the alarm itself runs the work).
async function scheduleWatchdog(env) {
const rows = (await env.DB.prepare("SELECT tenant_id, channel, channel_id FROM resident_schedules WHERE next_run IS NOT NULL AND next_run < ?").bind(now() - 120e3).all()).results;
for (const r of rows) { try { await env.RESIDENT.get(env.RESIDENT.idFromName(`res:${r.tenant_id}:${r.channel}`)).ensure(r.tenant_id, r.channel, r.channel_id); } catch (e) { console.log("watchdog", r.channel, e.message); } }
return { overdue: rows.length };
}
// ---- Your own address: <slug>.1helm.com. Cloud and self-hosted 1Helm share the namespace (self-hosted rows live in the
// 1helm-workspaces D1, bound as SELFHOST), so a name is free only if neither has it.
async function slugCheck(env, slug, tenantId = null) {
slug = String(slug || "").trim().toLowerCase();
if (!slugValid(slug)) return { slug, available: false, reason: RESERVED.has(slug) ? "That address is reserved. Try another." : "Use 3–48 letters, numbers or single dashes." };
const mine = await env.DB.prepare("SELECT id FROM tenants WHERE slug=?").bind(slug).first();
if (mine && mine.id !== tenantId) return { slug, available: false, reason: "That address is taken. Try another." };
if (env.SELFHOST) { const sh = await env.SELFHOST.prepare("SELECT 1 FROM workspaces WHERE slug=?").bind(slug).first().catch(() => null); if (sh) return { slug, available: false, reason: "That address is taken. Try another." }; }
return { slug, available: true };
}
// Point a configured workspace subdomain at this Worker. Requires an API token with domain access.
async function claimHost(env, t, oldSlug = null) {
if (env.ENV_NAME === "dev" || !eeEnabled(env)) return false;
if (!env.CF_API_TOKEN || !env.CF_ACCOUNT_ID || !env.CF_ZONE_ID || !env.PUBLIC_BASE_DOMAIN || !env.WORKER_SERVICE_NAME) { console.log("claimHost: custom domain not configured; address saved, host not created", t.slug); return false; }
const H = { authorization: `Bearer ${env.CF_API_TOKEN}`, "content-type": "application/json" };
const hostname = (slug) => `${slug}.${env.PUBLIC_BASE_DOMAIN}`;
try {
const r = await fetch(`https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/workers/domains`, { method: "PUT", headers: H, body: JSON.stringify({ hostname: hostname(t.slug), service: env.WORKER_SERVICE_NAME, zone_id: env.CF_ZONE_ID, environment: "production" }) });
const d = await r.json().catch(() => ({}));
if (!d.success) { console.log("claimHost failed", t.slug, JSON.stringify(d.errors || d).slice(0, 300)); return false; }
await env.DB.prepare("UPDATE tenants SET host_ready=1,updated_at=? WHERE id=?").bind(now(), t.id).run();
if (oldSlug && oldSlug !== t.slug) {
const l = await (await fetch(`https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/workers/domains?hostname=${encodeURIComponent(hostname(oldSlug))}`, { headers: H })).json().catch(() => ({}));
for (const x of l.result || []) if (x.hostname === hostname(oldSlug) && x.service === env.WORKER_SERVICE_NAME) await fetch(`https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/workers/domains/${x.id}`, { method: "DELETE", headers: H });
}
return true;
} catch (e) { console.log("claimHost error", e.message); return false; }
}
// Where a finished account opens: its own address when that host is live (sign-in is handed over with a one-time token,
// so the session cookie never has to be shared with other hosts), else /w/<slug>/app.
const nativeMobile = (req) => /(?:^|\s)1HelmMobile(?:\s|$)/.test(req?.headers.get("user-agent") || "");
const homeUrl = (t, next = "", req = null) => Number(t.host_ready) === 1 && !nativeMobile(req) ? `/auth/to?next=${encodeURIComponent(next || `/w/${t.slug}/app`)}` : (next || `/w/${t.slug}/app`);
// ---- The approved onboarding (v1) on real data. Steps before payment run in the page and save here as they're chosen;
// payment is Stripe Checkout; after payment the account is provisioned and the person finishes Ways to reach → Watch it work → First task.
async function uniqueSlug(env, base) {
let b = String(base || "").toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 30);
if (b.length < 3 || /^\d+$/.test(b) || RESERVED.has(b)) b = `my-${b || "helm"}`.replace(/-+$/, "");
for (let i = 0; i < 8; i++) {
const cand = i === 0 ? b : `${b}-${100 + Math.floor(Math.random() * 900)}`;
if ((await slugCheck(env, cand)).available) return cand;
}
return `helm-${rand(4)}`;
}
async function ensureTenant(env, user, first) {
let t = await tenantOf(env, user.id); if (t) return t;
const id = crypto.randomUUID(), slug = await uniqueSlug(env, first || user.email.split("@")[0]);
await env.DB.prepare("INSERT OR IGNORE INTO tenants (id,slug,owner_user_id,created_at,updated_at) VALUES (?,?,?,?,?)").bind(id, slug, user.id, now(), now()).run();
return tenantOf(env, user.id);
}
async function ensureSelfHostedTenant(env, user) {
let t = await ensureTenant(env, user);
if (t.status === "active" && onboarded(t)) return t;
await archive(env, t.id).provisionFresh();
const first = cleanName(user.email.split("@")[0].replace(/[._-]+/g, " "), 80) || "Owner";
await env.DB.prepare("UPDATE tenants SET status='active',plan=?,first_name=CASE WHEN first_name='' THEN ? ELSE first_name END,helper_name=CASE WHEN helper_name='' THEN ? ELSE helper_name END,slug_chosen=1,onboarding_completed=1,onboard_step='done',provisioned_at=COALESCE(provisioned_at,?),updated_at=? WHERE id=?")
.bind(BYOK_PLAN, first, DEFAULT_HELPER, now(), now(), t.id).run();
return tenantOf(env, user.id);
}
// Check before reading a new email body. A pending pre-deploy sign-in can finish/resend its own email, never another address.
async function ageAccess(req, env, { allowPending = true } = {}) {
const age = await ageOf(req, env);
if (age) return { age, allowed: true };
if (await currentUser(req, env)) return { allowed: true };
if (!allowPending) return { allowed: false };
const pending = await pendingEmail(env, req);
return { allowed: !!pending, pending };
}
async function onboardApi(req, env, url, op) {
const origin = httpsOrigin(url);
let body = {};
try {
let access;
if (req.method === "POST" && op !== "code") {
const o = req.headers.get("origin"); if (o && o !== origin) return json({ error: "Not allowed." }, 403);
access = eeEnabled(env) ? await ageAccess(req, env) : { allowed: true };
if (!access.allowed) return json(AGE_ERROR, 403);
}
if (req.method === "POST") body = await boundedJson(req, 32_768);
if (op === "email" && req.method === "POST") {
const email = String(body.email || "").trim().toLowerCase();
if (access.pending && access.pending !== email) return json(AGE_ERROR, 403);
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) return json({ error: "Enter a valid email address." });
if (!eeEnabled(env) && !(await ownerAllowsEmail(env, email))) return json({ error: "This server already has an owner." }, 403);
const recent = await env.DB.prepare("SELECT COUNT(*) n FROM magic_links WHERE email=? AND expires_at>?").bind(email, now()).first();
if (Number(recent?.n || 0) > 5) return json({ error: "We just sent you a few links. Check your inbox (and spam) for the newest one." });
// One email, two ways in: the link and a 6-digit code (src/signin-code.js). The code works only in this browser (hc_signin cookie).
const plan = PLANS[body.plan] ? body.plan : "", si = await createSignIn(env, email, { age: access.age });
const link = `${origin}/auth/verify?token=${si.token}${plan ? `&plan=${plan}` : ""}`, ck = { "set-cookie": pendingCookie(si.pending) };
if (eeEnabled(env) && env.TEST_MODE === "1" && /@example\.(com|org|net)$/.test(email)) return json({ ok: true, test_link: link, test_code: si.code, resend_wait: RESEND_WAIT }, 200, ck);
try { await sendMagicLink(env, email, link, si.code); } catch (e) { console.error("magic link send failed", e?.code, e?.message); return json({ error: "We couldn't send your sign-in email just now. Try again in a minute." }); }
return json({ ok: true, resend_wait: RESEND_WAIT }, 200, ck);
}
// The 6-digit code from the sign-in email, typed on this device. Same session as the link (newSession + hc_session).
if (op === "code" && req.method === "POST") {
const o = req.headers.get("origin"); if (o && o !== origin) return json({ error: "Not allowed." }, 403);
const r = await verifyCode(env, req, body.code);
if (!r.email) return json({ error: r.error, kind: r.kind });
return signedIn(env, req, r.email, { json: { ok: true }, age: r.age });
}
const user = await currentUser(req, env);
if (!user) {
// Waiting on a code in this browser (a reload, or the installed app coming back from Mail): back to the code screen.
const pe = op === "state" ? await pendingEmail(env, req) : "";
if (eeEnabled(env) && op === "state" && !pe && !await ageOf(req, env)) return json(AGE_ERROR, 403);
return json(pe ? { signed_in: false, step: "email", email: pe, pending: true, resend_wait: RESEND_WAIT } : { signed_in: false, step: "account" });
}
if (!eeEnabled(env)) {
const t = await ensureSelfHostedTenant(env, user);
return op === "state" ? json({ signed_in: true, next_url: homeUrl(t, "", req) }) : json({ error: "not found" }, 404);
}
let t = await tenantOf(env, user.id);
if (op === "slug") return json(await slugCheck(env, url.searchParams.get("value"), t?.id || null));
// The last step's Composio sign-in (GET, polled while the person signs in on Composio's page). It was below the POST-only line, so the
// poll got a 404 and the step sat on "Opening Composio…" (Captain, 10-07).
if (op === "apps-state") return t && t.status === "active" ? json(await onboardAppsChoice(env, t)) : json({ error: "Finish paying first." });
if (op === "state") {
if (t && t.status === "active" && !onboarded(t)) {
await archive(env, t.id).provisionFresh();
await env.DB.prepare("UPDATE tenants SET helper_name=CASE WHEN helper_name='' THEN ? ELSE helper_name END,onboarding_completed=1,onboard_step='reach',provisioned_at=COALESCE(provisioned_at,?),updated_at=? WHERE id=?").bind(DEFAULT_HELPER, now(), now(), t.id).run();
t = await tenantOf(env, user.id);
if (Number(t.slug_chosen) === 1 && Number(t.host_ready) !== 1) { await claimHost(env, t); t = await tenantOf(env, user.id); }
}
const profile = t ? { first: t.first_name || "", helperId: Number(t.helper_avatar || 8), helperName: t.helper_name || "", tone: Number(t.helper_tone || 0), plan: planKey(t.plan), slug: t.slug, slugChosen: Number(t.slug_chosen) === 1,
privacy: privacyView(t).chosen ? privacyView(t).level : "" } : {};
// AI data privacy (privacy.js): an explicit choice is required before payment (and before the first task); a paid account that
// never chose (it paid before this step existed) is asked right after its address. Unset always resolves to ZDR on the server.
const needPrivacy = t && !privacyView(t).chosen;
if (t && t.status === "active" && onboarded(t)) {
if (onboardDone(t)) return json({ signed_in: true, next_url: homeUrl(t, "", req) });
return json({ signed_in: true, email: user.email, paid: true, step: Number(t.slug_chosen) === 1 ? (needPrivacy ? "privacy" : t.onboard_step || "reach") : "address", profile, tenant: { status: t.status, slug: t.slug, host_ready: Number(t.host_ready) === 1, finished: t.onboard_step === "done" },
texting: await textingStatus(env, t.id), texting_url: `/w/${t.slug}/texting?from=onboard`,
apps: await onboardApps(env, t.id), apps_api: `/w/${t.slug}/api/apps/connect`, apps_choice: await onboardAppsChoice(env, t) });
}
if (t && t.status !== "pending_payment" && t.status !== "active") return json({ signed_in: true, error: `There's a problem with this account's payment. Open Settings in your 1Helm or contact ${env.SUPPORT_EMAIL || env.EMAIL_FROM}.` });
const step = !t || !t.first_name ? "hello" : !t.helper_name ? "pick" : Number(t.slug_chosen) !== 1 ? "address" : needPrivacy ? "privacy" : "plan";
return json({ signed_in: true, email: user.email, paid: false, step, profile, tenant: t ? { status: t.status } : null });
}
if (req.method !== "POST") return json({ error: "unknown" }, 404);
const locked = t && t.status === "active" && onboarded(t);
if (op === "profile") {
const first = cleanName(body.first, 80); if (!first) return json({ error: "Enter your first name." });
if (locked) return json({ ok: true });
t = await ensureTenant(env, user, first);
await env.DB.prepare("UPDATE tenants SET first_name=?,updated_at=? WHERE id=?").bind(first, now(), t.id).run();
return json({ ok: true });
}
if (op === "helper") {
const name = cleanName(body.name, 40), avatar = Number(body.avatar), tone = Math.max(0, Math.min(2, Number(body.tone) || 0));
if (!name || !Number.isInteger(avatar) || avatar < 1 || avatar > 9) return json({ error: "Choose a helper and give them a name." });
if (locked) return json({ ok: true });
t = await ensureTenant(env, user);
await env.DB.prepare("UPDATE tenants SET helper_name=?,helper_avatar=?,helper_tone=?,updated_at=? WHERE id=?").bind(name, avatar, tone, now(), t.id).run();
return json({ ok: true });
}
if (op === "address") {
t = await ensureTenant(env, user);
const c = await slugCheck(env, body.slug, t.id); if (!c.available) return json({ error: c.reason });
const old = t.slug;
if (old !== c.slug) {
const r = await env.DB.prepare("UPDATE tenants SET slug=?,updated_at=? WHERE id=? AND NOT EXISTS (SELECT 1 FROM tenants WHERE slug=? AND id<>?)").bind(c.slug, now(), t.id, c.slug, t.id).run();
if (!r.meta.changes) return json({ error: "That address was just taken. Try another." });
}
await env.DB.prepare("UPDATE tenants SET slug_chosen=1,host_ready=CASE WHEN ?=slug THEN host_ready ELSE 0 END,updated_at=? WHERE id=?").bind(old, now(), t.id).run();
t = await tenantOf(env, user.id);
// Paid accounts get their address right away; unpaid ones when payment confirms.
if (t.status === "active") { await claimHost(env, t, old); t = await tenantOf(env, user.id); }
return json({ ok: true, slug: t.slug, host_ready: Number(t.host_ready) === 1, ...(t.onboard_step === "done" ? { url: homeUrl(t, "", req) } : {}) });
}
// AI data privacy: one of the three levels, validated here (anything else refused, nothing stored). Before or after payment.
if (op === "privacy") {
const level = typeof body.level === "string" ? body.level : ""; if (!isPrivacyLevel(level)) return json({ error: "Choose one of the three options." });
t = await ensureTenant(env, user);
const r = await setPrivacy(env, t.id, level); return json({ ok: true, level: r.level });
}
if (op === "checkout") {
if (locked) return json({ url: "/onboard" });
const plan = PLANS[body.plan] ? body.plan : "plus", first = cleanName(body.first, 80), name = cleanName(body.name, 40) || DEFAULT_HELPER, avatar = Number(body.avatar) || 8, tone = Math.max(0, Math.min(2, Number(body.tone) || 0));
t = await ensureTenant(env, user, first);
if (!planPrices(env)[plan]) return json({ error: "That plan isn't available right now." });
if (Number(t.slug_chosen) !== 1) return json({ error: "Pick your address first." });
if (!privacyView(t).chosen) return json({ error: "Choose how AI providers may use your data first.", step: "privacy" });
await env.DB.prepare("UPDATE tenants SET plan=?,first_name=CASE WHEN ?<>'' THEN ? ELSE first_name END,helper_name=?,helper_avatar=?,helper_tone=?,updated_at=? WHERE id=?").bind(plan, first, first, name, avatar, tone, now(), t.id).run();
const cs = await stripe(env, "checkout/sessions", {
mode: "subscription", customer_email: user.email, client_reference_id: t.id,
"line_items[0][price]": planPrices(env)[plan], "line_items[0][quantity]": 1,
success_url: `${origin}/onboard?paid=1`, cancel_url: `${origin}/onboard`,
metadata: { app: "1hc-proto", tenant_id: t.id, plan }, subscription_data: { metadata: { app: "1hc-proto", tenant_id: t.id, plan } },
payment_method_collection: "always" });
return cs.url ? json({ url: cs.url }) : json({ error: "Checkout didn't open. Try again in a minute." });
}
if (!locked) return json({ error: "Finish paying first." });
if (op === "step") {
const step = String(body.step || ""); if (["reach", "device", "watch", "task", "know", "apps"].includes(step)) await env.DB.prepare("UPDATE tenants SET onboard_step=?,updated_at=? WHERE id=?").bind(step, now(), t.id).run();
return json({ ok: true });
}
// The last step (Managed vs Self-managed): the box must be checked; Self-managed also needs the Composio sign-in done.
if (op === "finish") {
const task = String(body.task || "").trim().slice(0, 4000);
if (body.apps) { const r = await saveOnboardingChoice(env, t.id, body.apps); if (r.error) return json(r); }
await env.DB.prepare("UPDATE tenants SET onboard_step='done',updated_at=? WHERE id=?").bind(now(), t.id).run();
// Get to know you: always offered as a task in General at the end of onboarding; "Start now" opens it, "Later" leaves it in the list.
const iv = await interviewEnsure(env, t, String(body.tz || "")).catch((e) => { console.log("interview", e.message); return null; });
const ivStart = body.interview === "start" && iv?.status === "open" && iv.session_id ? `#/main/s/${iv.session_id}` : "";
if (!task) return json({ url: homeUrl(t, `/w/${t.slug}/app?welcome=1${ivStart}`, req) });
// The first task starts on the real helper right now; the app opens on it. The turn keeps running if nobody watches.
const R = env.RESIDENT.get(env.RESIDENT.idFromName(`res:${t.id}:main`));
const rd = (await R.chatStream(t.id, "main", "1", task, { session: "new" })).getReader();
let buf = "", sid = null; const dec = new TextDecoder();
for (let i = 0; i < 20 && !sid; i++) { const { value, done } = await rd.read(); if (done) break; buf += dec.decode(value, { stream: true }); const m = buf.match(/"t":"session","session":\{"id":"([0-9a-f-]{36})"/); if (m) sid = m[1]; }
rd.cancel().catch(() => {});
return json({ url: homeUrl(t, `/w/${t.slug}/app?welcome=1${ivStart || (sid ? `#/main/s/${sid}` : "")}`, req) });
}
return json({ error: "unknown" }, 404);
} catch (e) { if (e instanceof BodyTooLargeError || e instanceof UnsupportedFormTypeError) throw e; console.error("onboard api", op, e?.message); return json({ error: "Something went wrong on our side. Try again in a minute." }); }
}
export default {
async scheduled(ev, env, ctx) {
env = tolerantEnv(env); // do-stub.js: every Durable Object call survives a deploy's skew/reset window
if (env.ENV_NAME === "selfhost" && env.BACKUP_MAINTENANCE === "1") return;
// Every minute: meeting reminders for notes only (src/meetings.js: D1 reads, and a few calendar looks when due). Nothing else runs on it.
if (ev.cron === "* * * * *") { if (eeEnabled(env)) ctx.waitUntil(meetingsCron(env, { now: ev.scheduledTime || Date.now() }).then((r) => { if (r.due.sent || r.due.missed || r.polled.length) console.log("meetings cron", JSON.stringify(r).slice(0, 600)); }).catch((e) => console.log("meetings cron failed", e.message))); return; }
if (ev.cron === "*/5 * * * *") ctx.waitUntil(deletionCron(env).then((r) => { if (r.resumed || r.errors || r.hs) console.log("deletion cron", JSON.stringify(r)); }).catch((e) => console.log("deletion cron failed", e.message)));
ctx.waitUntil(scheduleWatchdog(env).then((r) => console.log("watchdog", JSON.stringify(r))));
if (ev.cron === "17 * * * *") ctx.waitUntil(reconcile(env).then((r) => console.log("reconcile", JSON.stringify(r))));
// Sign-in codes: forget wrong-code counts older than a day (src/signin-code.js).
if (ev.cron === "17 * * * *") ctx.waitUntil(sweepCodeFails(env).catch((e) => console.log("code fails sweep failed", e.message)));
// Bee: restart the sync of a connected account that hasn't synced in 30 min (src/bee.js beeCron; the BeeSync alarm does the work).
if (ev.cron === "17 * * * *" && eeEnabled(env)) ctx.waitUntil(beeCron(env).then((r) => { if (r.checked) console.log("bee cron", JSON.stringify(r)); }).catch((e) => console.log("bee cron failed", e.message)));
// Storage: recount the accounts not counted in a day (corrects any drift in the running counter). src/storage.js
if (ev.cron === "17 * * * *") ctx.waitUntil(recountStale(env).then((r) => console.log("storage recount", JSON.stringify(r))).catch((e) => console.log("storage recount failed", e.message)));
// Cancelled plans: reminders 7/3/1 days before the period ends, deletion at the end if the webhook missed it, final emails (src/cancel.js).
if (ev.cron === "17 * * * *" && eeEnabled(env)) ctx.waitUntil(cancelCron(env, { stripe }).then((r) => console.log("cancel cron", JSON.stringify(r))).catch((e) => console.log("cancel cron failed", e.message)));
// Unpaid plans: first notices, reminders (start of the final 7 days, 1 day before), deletion when the 7 days are up (src/pastdue.js).
if (ev.cron === "17 * * * *" && eeEnabled(env)) ctx.waitUntil(pastdueCron(env, { stripe }).then((r) => console.log("pastdue cron", JSON.stringify(r))).catch((e) => console.log("pastdue cron failed", e.message)));
// Own router pool: rebuilt nightly at 2:17 AM PT (09:17 UTC) when the own router is on (dev only for now).
// Hindsight: delete banks whose account or channel no longer exists. Nightly at 3:17 AM PT (10:17 UTC).
if (ev.cron === "17 * * * *" && hsOn(env) && new Date(ev.scheduledTime).getUTCHours() === 10) ctx.waitUntil(hsSweep(env).then((r) => console.log("hindsight sweep", JSON.stringify({ ...r, skipped: r.skipped.length }))).catch((e) => console.log("hindsight sweep failed", e.message)));
// Background memory jobs (memjobs.js): upkeep (hourly after activity), people (daily), the nightly reflection (3-5 AM their time).
// Silent: they change memory only. Off with MEMJOBS="off".
if (ev.cron === "17 * * * *" && env.MEMJOBS !== "off") ctx.waitUntil(memjobsCron(env, { residentOf: (tid, ch) => env.RESIDENT.get(env.RESIDENT.idFromName(`res:${tid}:${ch}`)), now: ev.scheduledTime }).then((r) => console.log("memjobs cron", JSON.stringify(r))).catch((e) => console.log("memjobs cron failed", e.message)));
// Decision shadow rows (dev only): kept 7 days.
if (ev.cron === "17 * * * *" && env.ENV_NAME === "dev") ctx.waitUntil(pruneLab(env).then((n) => { if (n) console.log("router lab pruned", n); }).catch((e) => console.log("router lab prune failed", e.message)));
if (ev.cron === "17 * * * *" && eeEnabled(env) && env.ENV_NAME === "dev") ctx.waitUntil(pruneShadow(env).then((n) => { if (n) console.log("decision shadow pruned", n); }).catch((e) => console.log("decision shadow prune failed", e.message)));
if (ev.cron === "17 * * * *" && eeEnabled(env) && env.OWN_ROUTER && !route2On(env) && new Date(ev.scheduledTime).getUTCHours() === 9) ctx.waitUntil(saveNightlyPool(env).then((r) => console.log("router pool", JSON.stringify(r))).catch((e) => console.log("router pool failed", e.message)));
},
async fetch(req, env, ctx) {
env = tolerantEnv(env); // do-stub.js: every Durable Object call survives a deploy's skew/reset window
const res = transportHeaders(req, await this.route(req, env, ctx));
if (isPublicShare(env, new URL(req.url))) return res; // share links and published pages are the person's content: never touched
// Dev environment: a yellow DEV strip on every page, so dev can never be mistaken for Live.
if (env.ENV_NAME === "dev" && (res.headers.get("content-type") || "").includes("text/html"))
return new HTMLRewriter().on("body", { element(e) { e.prepend(`<div id="devstrip" style="position:fixed;z-index:2147483647;top:0;left:50%;transform:translateX(-50%);background:#f5bd3c;color:#1c1b19;font:600 11px/1 system-ui,sans-serif;letter-spacing:.08em;padding:4px 10px;border-radius:0 0 8px 8px;pointer-events:none">DEV · test data · nothing here is Live</div>`, { html: true }); } }).transform(res);
return res;
},
async route(req, env, ctx) {
const transport = transportGate(req); if (transport) return transport;
const url = new URL(req.url), p = url.pathname, origin = httpsOrigin(url);
try {
if (isBackupPath(p)) return backupControl(req, env, p);
if (env.ENV_NAME === "selfhost" && env.BACKUP_MAINTENANCE === "1")
return p === "/health" ? json({ ok: true, maintenance: true }) : json({ error: "Backup maintenance is active." }, 503);
if (!eeRouteEnabled(env, p)) return json({ error: "not found" }, 404);
// These cookie and sign-in endpoints are reached from the server's own pages. Reject a supplied
// sibling-site or sandboxed Origin before it can start or finish a sign-in or change onboarding state.
if (req.method === "POST" && (p === "/auth/start" || p === "/auth/code" || p === "/auth/logout" || p === "/onboard" || p.startsWith("/onboard/"))) {
const requestOrigin = req.headers.get("origin");
if (requestOrigin && requestOrigin !== origin) return json({ error: "forbidden" }, 403);
}
// Release controls, parity notes and the Kanban proxy use the admin's session cookie on dev.
// Require the exact page origin for writes, including requests from sibling 1Helm hosts.
if (env.ENV_NAME === "dev" && !["GET", "HEAD", "OPTIONS"].includes(req.method) &&
(p === "/kanban" || p.startsWith("/kanban/") || p === "/parity" || p.startsWith("/parity/") || p === "/releases" || p.startsWith("/releases/")) &&
req.headers.get("origin") !== origin) return json({ error: "forbidden" }, 403);
if (p === "/health") return json({ ok: true });
if (p === "/api/mobile/compatibility" && req.method === "GET") {
const users = await env.DB.prepare("SELECT 1 FROM users LIMIT 1").first();
const ready = await env.DB.prepare("SELECT 1 FROM tenants WHERE status='active' AND onboarding_completed=1 AND (COALESCE(onboard_step,'')='' OR (onboard_step='done' AND slug_chosen=1)) LIMIT 1").first();
// The packaged mobile gateway runs at capacitor://localhost or https://localhost and checks
// a selected remote server before navigating there. This public, cookie-free GET needs CORS.
return json({ product: "1Helm", mobile_api: 1, version: APP_V, has_users: !!users, setup_complete: !!ready, requires_https: true }, 200, { "access-control-allow-origin": "*" });
}
// Explicit re-confirmation is reachable even while an older email/code is pending ("Use a different email").
if (p === "/onboard/age" && req.method === "GET") return agePage({ next: `/onboard${url.search}` });
if (p === "/auth/start" && req.method === "POST" && req.headers.get("origin") && req.headers.get("origin") !== origin) return new Response("Not allowed.", { status: 403 });
// Only this explicit, same-origin acknowledgement sets the remembered browser proof. It receives no email or profile data.
if (p === "/onboard/age" && req.method === "POST") {
const o = req.headers.get("origin"), site = req.headers.get("sec-fetch-site");
if ((o && o !== origin) || site === "cross-site") return new Response("Not allowed.", { status: 403 });
const f = await boundedForm(req), next = ageNext(String(f.get("next") || "/onboard"));
if (f.get("adult") !== "yes" || f.get("version") !== AGE_VERSION) return agePage({ next, error: AGE_ERROR.error, status: 400 });
return new Response(null, { status: 303, headers: { location: next, "set-cookie": await ageCookie(env), "cache-control": "no-store" } });
}
// Company/site logos (logo.dev behind it): public on every host, so <img src="/logo/stripe.com"> works anywhere. src/logo.js
if (p.startsWith("/logo/")) return await logoRoute(req, env, ctx, url);
// Composio app logos (Connected accounts), cached in R2 + Cache API so a cold logos.composio.dev never leaves blank tiles. src/applogo.js
if (p.startsWith("/applogo/")) return await appLogoRoute(req, env, ctx, url);
// Share links (/f/<token>/<name>) and published pages (/p/<name>/) on the person's address, or /s/<slug>/… on the app host.
// Public, before any sign-in gate; never reads the session cookie. src/share.js, src/publish.js
if (isPublicShare(env, url)) { const sr = await servePublic(req, env, url); if (sr) return sr; }
// Dev ops "view as" (src/ops-read.js): that sign-in can only look; every send, change, purchase or private read stops here.
{ const vg = viewAsGate(req, url); if (vg) return vg; }
// The app's web app manifest (Add to Home Screen / Install app): public, no DB, same for any slug. src/pwa.js
{ const mf = p.match(MANIFEST_RE); if (mf && req.method === "GET") return manifestResponse(mf[1], env.ENV_NAME === "dev"); }
// The Web Push service worker (scope /w/<slug>/): public and DB-free like the manifest. src/pwa.js
if (SW_RE.test(p) && req.method === "GET") return swResponse();
// A configured workspace subdomain: sign-in is handed over from the app host with a one-time token.
{ const domain = String(env.PUBLIC_BASE_DOMAIN || "").toLowerCase();
const hm = domain && url.hostname.endsWith(`.${domain}`) ? [url.hostname, url.hostname.slice(0, -domain.length - 1)] : null;
if (hm && url.hostname !== env.APP_HOST) {
const ht = await env.DB.prepare("SELECT * FROM tenants WHERE slug=?").bind(hm[1]).first();
if (!ht) return page("Not found", `<p>There's no 1Helm at ${esc(url.hostname)}.</p><p><a href="https://${env.APP_HOST || url.hostname}/">Start your own 1Helm →</a></p>`);
if (p === "/auth/handoff") {
if (req.method !== "GET") return new Response("Method not allowed", { status: 405, headers: { allow: "GET" } });
const tok = await sha256(url.searchParams.get("token") || ""), next = String(url.searchParams.get("next") || "");
const ml = await env.DB.prepare("UPDATE magic_links SET used=1 WHERE token_hash=? AND used=0 AND expires_at>? RETURNING email").bind(tok, now()).first();
const hu = ml && await env.DB.prepare("SELECT * FROM users WHERE email=?").bind(ml.email).first();
if (!hu || hu.id !== ht.owner_user_id) return Response.redirect(`https://${env.APP_HOST || url.hostname}/`, 302);
const sess = await newSession(env, req, hu.id);
return new Response(null, { status: 302, headers: { location: next.startsWith(`/w/${ht.slug}/`) ? next : `/w/${ht.slug}/app`, "set-cookie": `hc_session=${sess}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=2592000` } });
}
// Signed out (or someone else) at a person's own address → THEIR sign-in page, never the sign-up flow.
const hu = await currentUser(req, env), owner = hu && hu.id === ht.owner_user_id;
const hostInfo = { host: url.hostname, appHost: env.APP_HOST || url.hostname, helper: helperName(ht), avatar: Number(ht.helper_avatar || 8) };
if (p === "/auth/start" && req.method === "POST") {
// Real and decoy pending cookies must never change this page's appearance or reveal the owner's email.
const access = await ageAccess(req, env, { allowPending: false });
if (!access.allowed) return agePage({ next: "/", error: AGE_ERROR.error, status: 403 });
const email = String((await boundedForm(req)).get("email") || "").trim().toLowerCase();
if (access.pending && access.pending !== email) return agePage({ next: "/", error: AGE_ERROR.error, status: 403 });
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) return hostSignInPage({ ...hostInfo, email, error: "Enter a valid email address." });
const ou = await env.DB.prepare("SELECT email FROM users WHERE id=?").bind(ht.owner_user_id).first();
let testLink = "", testCode = "", pending = decoyPending();
// Only the owner's email gets a link and code; everyone sees the same "check your email" and gets a sign-in cookie
// (a decoy that matches nothing for any other address), so there's no hint whose 1Helm this is.
if (ou && ou.email === email) {
const recent = await env.DB.prepare("SELECT COUNT(*) n FROM magic_links WHERE email=? AND expires_at>?").bind(email, now()).first();
if (Number(recent?.n || 0) <= 5) {
const si = await createSignIn(env, email, { age: access.age }); pending = si.pending;
const link = `${origin}/auth/verify?token=${si.token}`;
if (eeEnabled(env) && env.TEST_MODE === "1" && /@example\.(com|org|net)$/.test(email)) { testLink = link; testCode = si.code; }
else { try { await sendMagicLink(env, email, link, si.code); } catch (e) { console.error("host magic link", e?.message); return hostSignInPage({ ...hostInfo, email, error: "We couldn't send the email just now. Try again in a minute." }); } }
}
}
return withCookie(hostSignInPage({ ...hostInfo, email, sent: true, testLink, testCode, wait: RESEND_WAIT }), pendingCookie(pending));
}
// The code typed on the "Check your email" page at the person's own address.
if (p === "/auth/code" && req.method === "POST") {
const f = await boundedForm(req), email = String(f.get("email") || "").trim().toLowerCase().slice(0, 200);
const r = await verifyCode(env, req, f.get("code"));
if (!r.email) return hostSignInPage({ ...hostInfo, email, sent: true, codeError: r.error, codeKind: r.kind, wait: 0 });
const cu = await env.DB.prepare("SELECT id FROM users WHERE email=?").bind(r.email).first();
if (!cu || cu.id !== ht.owner_user_id) return hostSignInPage({ ...hostInfo, email, sent: true, codeError: "That code isn't right. Check the newest email from 1Helm and try again.", codeKind: "wrong", wait: 0 });
return signedIn(env, req, r.email, { location: `/w/${ht.slug}/app`, age: r.age });
}
if (!owner && (p === "/" || p === "/onboard") && !(await ageAccess(req, env, { allowPending: false })).allowed) return agePage({ next: "/" });
if (p === "/" || p === "/onboard") return owner ? Response.redirect(`${origin}/w/${ht.slug}/app${url.search}`, 302) : hostSignInPage(hostInfo);
const open = p === "/auth/verify" || p === "/auth/code" || p === "/auth/handoff" || p === "/auth/logout" || p === "/auth/email-change" || p === "/health" || p.startsWith("/hooks/") || p === "/stripe/webhook" ||
p.startsWith("/app-static/") || p.startsWith("/onboard/") || p.startsWith("/fonts/") || p.startsWith("/av/") || p.startsWith("/c/") || /^\/1helm-(icon|sailboat)/.test(p);
if (!owner && !open) {
if (p.includes("/api/")) return json({ error: "signed_out" }, 401);
if (!(await ageAccess(req, env, { allowPending: false })).allowed) return agePage({ next: "/" });
return hostSignInPage(hostInfo);
}
if (p.startsWith("/w/") && !p.startsWith(`/w/${ht.slug}/`) && !p.startsWith(`/w/${ht.slug}`)) return Response.redirect(`https://${env.APP_HOST || url.hostname}${p}${url.search}`, 302);
}
}
{ const ph = p.match(/^\/hooks\/photon\/([0-9a-f]{32})$/); if (ph && req.method === "POST") return textingWebhook(req, env, ctx, ph[1]); }
// Hosted card page (a card reply sent by text opens here inside Messages; no login, token-gated). See src/cards.js.
if (p.startsWith("/c/")) { const cr = await cardRoute(req, env, ctx, url); if (cr) return cr; }
// The app catalog for the Composio version this server uses (connect-offer.js catalogFile): same URL, the latest list when
// COMPOSIO_TOOLKIT_VERSION="latest".
if (env.ASSETS && p === "/app-static/integrations.json" && catalogFile(env) !== "integrations.json") { const u = new URL(req.url); u.pathname = `/app-static/${catalogFile(env)}`; return env.ASSETS.fetch(new Request(u, req)); }
if ((p === "/onboard/v1/" || p === "/onboard/v1/index.html") && !(await ageAccess(req, env, { allowPending: false })).allowed) return agePage();
if (env.ASSETS && (p.startsWith("/app-static/") || p.startsWith("/onboard-static/") || p.startsWith("/onboard/v1/") || p === "/onboard/kit.js" || p === "/onboard/kit.css" || p.startsWith("/fonts/") || p.startsWith("/av/") || p === "/1helm-sailboat.png" || PWA_ICON_RE.test(p))) return env.ASSETS.fetch(req);
if (p === "/admin/reconcile") {
if (req.method !== "POST" || env.ENV_NAME !== "dev" || env.TEST_MODE !== "1" || req.headers.get("origin") !== origin || !isReleaseAdmin(env, await currentUser(req, env)))
return json({ error: "not found" }, 404);
return json(await reconcile(env));
}
// Models have their own admin gate on both environments. Writes only touch this environment's D1.
if (p === "/models" || p.startsWith("/models/")) return modelsRoute(req, env, await currentUser(req, env));
// Router lab settings (src/router-lab.js): dev only, release admins, same-origin JSON only (CSRF: the Origin must be this site and the
// x-router-lab header set, which a cross-site form can't send). GETs are no-store; /_ops stays read-only.
if (env.ENV_NAME === "dev" && p.startsWith("/router/lab/")) {
const u = await currentUser(req, env), H = { "content-type": "application/json", "cache-control": "no-store" }, J = (o, s = 200) => new Response(JSON.stringify(o), { status: s, headers: H });
if (!isReleaseAdmin(env, u)) return J({ error: u ? "not_found" : "signed_out" }, u ? 404 : 401);
return routerLabApi(req, env, url, { origin, by: u?.email || null, json: J });
}
// Decision shadow: Jev vs Perplexity's decider, live (src/decision-shadow.js, src/router-page.js). Dev only, release admins, GET only.
if (env.ENV_NAME === "dev" && (p === "/router" || p === "/router/data")) {
const u = await currentUser(req, env), H = { "cache-control": "no-store" };
if (!isReleaseAdmin(env, u)) {
if (p === "/router/data") return new Response(JSON.stringify({ error: u ? "not_found" : "signed_out" }), { status: u ? 404 : 401, headers: { "content-type": "application/json", ...H } });
return u ? new Response("Not found", { status: 404, headers: H }) : new Response(null, { status: 302, headers: { location: `${origin}/onboard?next=/router`, ...H } });
}
if (req.method !== "GET" && req.method !== "HEAD") return new Response("Method not allowed", { status: 405, headers: { allow: "GET", ...H } });
return p === "/router" ? routerPage(env, url) : routerData(env, url);
}
// Hindsight private-link self-test (dev only; needs the Hindsight API key as a bearer token).
if (env.ENV_NAME === "dev" && p === "/_ops/hindsight-selftest") {
if (!env.HINDSIGHT_API_KEY || req.headers.get("authorization") !== `Bearer ${env.HINDSIGHT_API_KEY}`) return new Response("Not found", { status: 404 });
if (req.method !== "POST") return new Response("Method not allowed", { status: 405, headers: { allow: "POST" } });
try { return Response.json(await hindsightSelfTest(env)); } catch (e) { return Response.json({ error: e.message }, { status: 502 }); }
}
// Hindsight bank sweep, on demand (dev only; same bearer key). ?dry=1 reports what would be deleted.
if (env.ENV_NAME === "dev" && p === "/_ops/hindsight-sweep" && req.method === "POST") {
if (!env.HINDSIGHT_API_KEY || req.headers.get("authorization") !== `Bearer ${env.HINDSIGHT_API_KEY}`) return new Response("Not found", { status: 404 });
try { return Response.json(await hsSweep(env, { dry: url.searchParams.get("dry") === "1", graceMs: url.searchParams.has("grace") ? Number(url.searchParams.get("grace")) : undefined })); } catch (e) { return Response.json({ error: e.message }, { status: 502 }); }
}
// Dev debugging (src/ops-read.js): read-only JSON on any dev account, and a look-only "view as". Dev + x-ops-key only; else 404.
if (p.startsWith("/_ops/")) return opsRoute(req, env, url, { routerPage, shadowData, labData, archive, ledger, storageOf, storageView, textingStatus, listConnections, holdOf, hsOn, hsRecall, bankId: hsBankId, beeOps });
// Kanban board (dev only, release admins): Kan behind Workers VPC.
if (env.ENV_NAME === "dev" && (p === "/kanban" || p.startsWith("/kanban/"))) {
// Kan's public app shell files (no board data) load without our cookie: browsers fetch the manifest without credentials.
if (/^\/kanban\/(manifest\.json|favicon\.ico|[\w-]+\.(?:png|svg)|_next\/static\/.+)$/.test(p)) return kanbanProxy(req, env, url, null);
const u = await currentUser(req, env);
if (!isReleaseAdmin(env, u)) return u ? page("Not found", "<p>Not found.</p>") : Response.redirect(`${origin}/onboard?next=/kanban/`, 302);
return kanbanProxy(req, env, url, u);
}
// Muse ↔ 1HC review (dev only, release admins): side-by-side docs + the Captain's notes.
if (env.ENV_NAME === "dev" && (p === "/parity" || p.startsWith("/parity/"))) {
const u = await currentUser(req, env);
if (!isReleaseAdmin(env, u)) return u ? page("Not found", "<p>Not found.</p>") : Response.redirect(`${origin}/onboard?next=/parity`, 302);
return parityRoute(req, env, url, u);
}
// Releases + Push (dev only).
if (env.ENV_NAME === "dev" && (p === "/releases" || p.startsWith("/releases/"))) {
const u = await currentUser(req, env);
if (!isReleaseAdmin(env, u)) return u ? page("Not found", "<p>Not found.</p>") : Response.redirect(`${origin}/onboard?next=/releases`, 302);
const m = p.match(/^\/releases\/(\d+)\/(push|undo)$/);
if (m && req.method === "POST") { let msg; try { msg = m[2] === "push" ? await pushRelease(env, Number(m[1])) : await undoRelease(env, Number(m[1])); } catch (e) { msg = "Didn't work: " + e.message; } return releasesPage(env, msg); }
return releasesPage(env);
}
// Report an issue (src/reports.js): the admins' list and each screenshot. Same gate as /releases.
if (env.ENV_NAME === "dev" && (p === "/reports" || p.startsWith("/reports/"))) {
const u = await currentUser(req, env);
if (!isReleaseAdmin(env, u)) return u ? page("Not found", "<p>Not found.</p>") : Response.redirect(`${origin}/onboard?next=/reports`, 302);
const rm = p.match(/^\/reports\/([0-9a-f-]{36})\/shot$/); if (rm) return reportShot(env, rm[1]);
return reportsPage(env);
}
if (p === "/" && req.method === "GET") {
const u = await currentUser(req, env);
if (u) {
const t = await tenantOf(env, u.id);
if (!t && env.ENV_NAME === "dev" && isReleaseAdmin(env, u)) return Response.redirect(`${origin}/releases`, 302);
return Response.redirect(`${origin}${t && t.status === "active" && onboarded(t) && onboardDone(t) ? homeUrl(t, "", req) : "/onboard"}`, 302);
}
return Response.redirect(`${origin}/onboard${url.search}`, 302);
}
if (p === "/auth/start" && req.method === "POST") {
const access = eeEnabled(env) ? await ageAccess(req, env) : { allowed: true };
if (!access.allowed) return agePage({ error: AGE_ERROR.error, status: 403 });
const email = String((await boundedForm(req)).get("email") || "").trim().toLowerCase();
if (access.pending && access.pending !== email) return agePage({ error: AGE_ERROR.error, status: 403 });
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) return eeEnabled(env) ? landingPage({ error: "Enter a valid email address.", email }) : await selfHostedPage(env, { error: "Enter a valid email address.", email });
if (!eeEnabled(env) && !(await ownerAllowsEmail(env, email))) return page("Sign-in closed", "<p>This server already has an owner.</p>");
// Real delivery via the configured Cloudflare Email Service. Reserved test addresses (@example.com) in TEST_MODE get an on-screen shortcut.
const testAddr = eeEnabled(env) && env.TEST_MODE === "1" && /@example\.(com|org|net)$/.test(email);
if (!testAddr) {
const recent = await env.DB.prepare("SELECT COUNT(*) n FROM magic_links WHERE email=? AND expires_at>?").bind(email, now()).first();
if (Number(recent?.n || 0) > 5) return checkEmailPage(email, "", false, { error: "We just sent you a few links. Check your inbox (and spam) for the newest one.", wait: 0, emailFrom: env.EMAIL_FROM });
}
const si = await createSignIn(env, email, { age: access.age }), link = `${origin}/auth/verify?token=${si.token}`;
if (testAddr) return withCookie(checkEmailPage(email, link, true, { testCode: si.code, wait: RESEND_WAIT, emailFrom: env.EMAIL_FROM }), pendingCookie(si.pending));
try { await sendMagicLink(env, email, link, si.code); }
catch (e) { console.error("magic link send failed", e?.code, e?.message); return eeEnabled(env) ? landingPage({ error: "We couldn't send your sign-in email just now. Try again in a minute.", email }) : await selfHostedPage(env, { error: "We couldn't send your sign-in email just now. Try again in a minute.", email }); }
return withCookie(checkEmailPage(email, "", false, { wait: RESEND_WAIT, emailFrom: env.EMAIL_FROM }), pendingCookie(si.pending));
}
// The code from the sign-in email, typed on the "Check your email" page (src/signin-code.js).
if (p === "/auth/code" && req.method === "POST") {
const f = await boundedForm(req), email = String(f.get("email") || "").trim().toLowerCase().slice(0, 200);
const r = await verifyCode(env, req, f.get("code"));
if (!r.email) return checkEmailPage(email, "", false, { codeError: r.error, codeKind: r.kind, wait: 0, emailFrom: env.EMAIL_FROM });
return signedIn(env, req, r.email, { location: `${origin}/`, age: r.age });
}
if (p === "/auth/to" && req.method === "GET") {
const u = await currentUser(req, env), t = u && await tenantOf(env, u.id), next = String(url.searchParams.get("next") || "");
if (!u || !t) return Response.redirect(`${origin}/onboard`, 302);
const path = next.startsWith(`/w/${t.slug}/`) ? next : `/w/${t.slug}/app`;
if (Number(t.host_ready) !== 1 || !eeEnabled(env) || !env.PUBLIC_BASE_DOMAIN || url.hostname !== env.APP_HOST || nativeMobile(req)) return Response.redirect(`${origin}${path}`, 302);
const tok = rand(); await env.DB.prepare("INSERT INTO magic_links (token_hash,email,expires_at) VALUES (?,?,?)").bind(await sha256(tok), u.email, now() + 60e3).run();
return Response.redirect(`https://${t.slug}.${env.PUBLIC_BASE_DOMAIN}/auth/handoff?token=${tok}&next=${encodeURIComponent(path)}`, 302);
}
if (p === "/auth/logout" && req.method === "POST") {
// A failed revoke must not look like a logout: the browser keeps its cookie and is told to try again.
try { await endSession(env, req); }
catch { return json({ error: "We couldn't log you out. Please try again." }, 503); }
return new Response(null, { status: 204, headers: { "set-cookie": "hc_session=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0", "cache-control": "no-store" } });
}
// Changing the sign-in email: the one-time link sent to the new address (src/account.js). Works signed out, on any device.
if (p === "/auth/email-change") return await emailChangeRoute(req, env, url, { stripe });
if (p === "/auth/verify") {
if (req.method !== "GET") return new Response("Method not allowed", { status: 405, headers: { allow: "GET" } });
const h = await sha256(url.searchParams.get("token") || "");
const ml = await env.DB.prepare("UPDATE magic_links SET used=1 WHERE token_hash=? AND used=0 AND expires_at>? RETURNING email,age_version,age_confirmed_at").bind(h, now()).first();
if (!ml) return expiredPage();
const pl = url.searchParams.get("plan");
return signedIn(env, req, ml.email, { location: eeEnabled(env) && PLANS[pl] ? `${origin}/onboard?plan=${pl}` : `${origin}/`, status: 302, age: ml.age_version ? { version: ml.age_version, at: ml.age_confirmed_at } : null });
}
// The approved onboarding is the sign-up page for everyone ("Sign up" / "Choose plan").
if (p === "/onboard" && req.method === "GET") {
const u = await currentUser(req, env), t = u && await tenantOf(env, u.id);
if (!eeEnabled(env)) return u && t ? Response.redirect(`${origin}${homeUrl(t, "", req)}`, 302) : await selfHostedPage(env);
if (t && t.status === "active" && onboarded(t) && onboardDone(t)) return Response.redirect(`${origin}${homeUrl(t, "", req)}`, 302);
if (!u && !(await ageAccess(req, env)).allowed) return agePage({ next: `/onboard${url.search}` });
const r = await env.ASSETS.fetch(new Request(`${origin}/onboard/v1/index.html`));
return new Response((await r.text()).replaceAll("__V__", ONBOARD_V), { headers: { "content-type": "text/html;charset=utf-8", "cache-control": "no-store", "x-robots-tag": "noindex" } });
}
if (p.startsWith("/onboard/api/")) return await onboardApi(req, env, url, p.slice("/onboard/api/".length));
if (!eeEnabled(env) && (p === "/onboard" || p.startsWith("/onboard/"))) return json({ error: "not found" }, 404);
const imp = p.match(/^\/import\/([0-9a-f-]{36})\.([0-9a-f]{16})(\/.*)$/);
if (imp) return handleImport(req, env, imp[1], `${imp[1]}.${imp[2]}`, imp[3]);
// Composio's project webhook (stage 5): app events for "when … happens" workflows. No cookie: the webhook signature is the check.
if (p === "/composio/webhook") return composioWebhook(req, env, ctx);
const user = await currentUser(req, env);
if (!user && p !== "/stripe/webhook") return Response.redirect(`${origin}/`, 302);
// Back from signing in to the person's own Composio account (Self-managed apps, composio-self.js). The state must match the cookie
// this browser got when it started, and belong to the signed-in person's workspace.
if (p === "/composio/callback" && req.method === "GET") {
const t = await tenantOf(env, user.id), sp = url.searchParams;
const cookieState = (req.headers.get("cookie") || "").match(/(?:^|;\s*)hc_cmp=([A-Za-z0-9_-]+)/)?.[1] || "";
const r = await signInFinish(env, { code: sp.get("code") || "", state: sp.get("state") || "", error: sp.get("error") || "", cookieState, tenantId: t?.id || "" }).catch((e) => ({ error: "Composio didn't finish the sign-in. Try again.", why: e.message }));
if (r.why) console.log("composio callback", String(r.why).slice(0, 160));
const clear = { "set-cookie": "hc_cmp=; Path=/composio; HttpOnly; Secure; SameSite=Lax; Max-Age=0", "cache-control": "no-store" };
const back = String(r.back || "");
if (back === "close" || !t) return new Response(composioDonePage(r, ONBOARD_V), { headers: { "content-type": "text/html;charset=utf-8", ...clear } });
const to = back === "/onboard" || back.startsWith(`/w/${t.slug}/`) ? back : `/w/${t.slug}/app?settings=accounts`;
const q = r.ok ? "cmp=ok" : `cmp=failed&status=failed&cmperr=${encodeURIComponent(r.error || "")}`;
return new Response(null, { status: 302, headers: { location: `${origin}${to}${to.includes("?") ? "&" : "?"}${q}`, ...clear } });
}
if (p === "/onboard/status" && req.method === "GET") {
const t = await tenantOf(env, user.id), next = onboardNext(t);
return json({ status: t?.status || "new", next });
}
if (p === "/onboard/slug" && req.method === "GET") {
const slug = String(url.searchParams.get("value") || "").trim().toLowerCase();
if (!slugValid(slug)) return json({ available: false, reason: "Use 3–48 lowercase letters, numbers, or single hyphens." });
const t = await tenantOf(env, user.id), taken = await env.DB.prepare("SELECT owner_user_id FROM tenants WHERE slug=?").bind(slug).first();
return json({ available: !taken || taken.owner_user_id === user.id, reason: taken && taken.owner_user_id !== user.id ? "That address is already taken." : null, current: t?.slug || null });
}
if ((p === "/onboard/profile" || p === "/onboard/helper" || p === "/onboard/plan") && req.method === "GET") return Response.redirect(`${origin}/onboard${url.search}`, 302);
if (p === "/onboard/plan" && req.method === "GET") {
const t = await tenantOf(env, user.id);
if (!t) return Response.redirect(`${origin}/onboard`, 302);
if (t.status === "active") return Response.redirect(`${origin}${onboardNext(t) || "/onboard"}`, 302);
return onboardPage("plan", { slug: t.slug, domain: env.PUBLIC_BASE_DOMAIN, plan: planKey(t.plan), plans: PLANS, testMode: env.STRIPE_MODE !== "live" });
}
if (p === "/onboard/plan" && req.method === "POST") {
const t = await tenantOf(env, user.id), plan = String((await boundedForm(req)).get("plan") || "");
if (!t) return Response.redirect(`${origin}/onboard`, 303);
if (t.status === "active") return Response.redirect(`${origin}${onboardNext(t) || "/onboard"}`, 303);
if (!privacyView(t).chosen) return Response.redirect(`${origin}/onboard`, 303); // the AI data privacy choice comes first (the v1 onboarding asks it)
if (!PLANS[plan] || !planPrices(env)[plan]) return onboardPage("plan", { slug: t.slug, domain: env.PUBLIC_BASE_DOMAIN, plan: planKey(t.plan), plans: PLANS, testMode: env.STRIPE_MODE !== "live", error: "Choose one of the plans." });
await env.DB.prepare("UPDATE tenants SET plan=?,updated_at=? WHERE id=?").bind(plan, now(), t.id).run();
const cs = await stripe(env, "checkout/sessions", {
mode: "subscription", customer_email: user.email, client_reference_id: t.id,
"line_items[0][price]": planPrices(env)[plan], "line_items[0][quantity]": 1,
success_url: `${origin}/onboard?paid=1`, cancel_url: `${origin}/onboard/plan`,
metadata: { app: "1hc-proto", tenant_id: t.id, plan }, subscription_data: { metadata: { app: "1hc-proto", tenant_id: t.id, plan } },
payment_method_collection: "always" });
return Response.redirect(cs.url, 303);
}
if ((p === "/onboard" || p === "/onboard/profile" || p === "/onboard/helper") && req.method === "GET") {
const t = await tenantOf(env, user.id);
if (t && t.status === "active" && onboarded(t)) return Response.redirect(`${origin}/w/${t.slug}/app`, 302);
if (p === "/onboard" && url.searchParams.get("paid") && t?.status !== "active") return onboardPage("waiting");
if (!t || t.status !== "active") return onboardPage("address", { slug: t?.slug || "", plan: t?.plan || "plus", domain: env.PUBLIC_BASE_DOMAIN });
if (p === "/onboard/helper" && t.first_name) return onboardPage("helper", { helperName: helperName(t), avatar: t.helper_avatar || 8 });
if (t.first_name && p === "/onboard") return Response.redirect(`${origin}/onboard/helper`, 302);
return onboardPage("profile", { firstName: t.first_name || "" });
}
if (p === "/onboard" && req.method === "POST") {
const fd = await boundedForm(req), slug = String(fd.get("slug") || "").trim().toLowerCase(), plan = planKey(String(fd.get("plan") || "plus"));
if (!slugValid(slug)) return onboardPage("address", { slug, plan, domain: env.PUBLIC_BASE_DOMAIN, error: "Use 3–48 lowercase letters, numbers, or single hyphens." });
let t = await tenantOf(env, user.id);
if (t?.status === "active") return Response.redirect(`${origin}${onboardNext(t) || "/onboard"}`, 303);
if (!t) {
const id = crypto.randomUUID();
const r = await env.DB.prepare("INSERT INTO tenants (id,slug,owner_user_id,created_at,updated_at) SELECT ?,?,?,?,? WHERE NOT EXISTS (SELECT 1 FROM tenants WHERE slug=?)").bind(id, slug, user.id, now(), now(), slug).run();
if (!r.meta.changes) return onboardPage("address", { slug, plan, domain: env.PUBLIC_BASE_DOMAIN, error: "That address is already taken." });
t = { id, slug };
} else if (t.slug !== slug) {
const r = await env.DB.prepare("UPDATE tenants SET slug=?,updated_at=? WHERE id=? AND NOT EXISTS (SELECT 1 FROM tenants WHERE slug=?)").bind(slug, now(), t.id, slug).run();
if (!r.meta.changes) return onboardPage("address", { slug, plan, domain: env.PUBLIC_BASE_DOMAIN, error: "That address is already taken." });
t = { ...t, slug };
}
if (!fd.get("plan")) return Response.redirect(`${origin}/onboard/plan`, 303);
await env.DB.prepare("UPDATE tenants SET plan=?,updated_at=? WHERE id=?").bind(plan, now(), t.id).run();
const cs = await stripe(env, "checkout/sessions", {
mode: "subscription", customer_email: user.email, client_reference_id: t.id,
"line_items[0][price]": planPrices(env)[plan], "line_items[0][quantity]": 1,
success_url: `${origin}/onboard?paid=1`, cancel_url: `${origin}/onboard`,
metadata: { app: "1hc-proto", tenant_id: t.id, plan }, subscription_data: { metadata: { app: "1hc-proto", tenant_id: t.id, plan } },
payment_method_collection: "always" });
return Response.redirect(cs.url, 303);
}
if (p === "/onboard/profile" && req.method === "POST") {
const t = await tenantOf(env, user.id);
if (!t || t.status !== "active") return Response.redirect(`${origin}/onboard`, 303);
if (onboarded(t)) return Response.redirect(`${origin}/w/${t.slug}/app`, 303);
const firstName = cleanName((await boundedForm(req)).get("first_name"), 80);
if (!firstName) return onboardPage("profile", { error: "Enter the name you want your helper to use." });
await env.DB.prepare("UPDATE tenants SET first_name=?,updated_at=? WHERE id=?").bind(firstName, now(), t.id).run();
return Response.redirect(`${origin}/onboard/helper`, 303);
}
if (p === "/onboard/helper" && req.method === "POST") {
const t = await tenantOf(env, user.id);
if (!t || t.status !== "active") return Response.redirect(`${origin}/onboard`, 303);
if (onboarded(t)) return Response.redirect(`${origin}/w/${t.slug}/app`, 303);
const f = await boundedForm(req), helperName = cleanName(f.get("helper_name"), 40), avatar = Number(f.get("helper_avatar"));
if (!helperName || !Number.isInteger(avatar) || avatar < 1 || avatar > 9) return onboardPage("helper", { helperName, avatar: avatar || 8, error: "Choose a helper and give them a name." });
await archive(env, t.id).provisionFresh();
await env.DB.prepare("UPDATE tenants SET helper_name=?,helper_avatar=?,onboarding_completed=1,provisioned_at=COALESCE(provisioned_at,?),updated_at=? WHERE id=?").bind(helperName, avatar, now(), now(), t.id).run();
return Response.redirect(`${origin}/w/${t.slug}/app?welcome=1`, 303);
}
if (p === "/stripe/webhook" && req.method === "POST") {
let raw;
try { raw = await boundedText(req, 1_000_000); }
catch (e) { return json({ error: e instanceof BodyTooLargeError ? "too large" : "bad body" }, e instanceof BodyTooLargeError ? 413 : 400); }
if (!(await verifyStripe(env, raw, req.headers.get("stripe-signature")))) return json({ error: "bad signature" }, 400);
const ev = JSON.parse(raw);
// A reserved ID is not delivered yet. A concurrent retry must not get a
// success response until the handler finishes; an abandoned reservation
// can be reclaimed after its lease so a crashed Worker cannot lose it.
const started = now(), reservation = -started;