@emulators/google 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -66,6 +66,7 @@ var LABEL_ALIASES = {
66
66
  updates: "CATEGORY_UPDATES",
67
67
  forums: "CATEGORY_FORUMS"
68
68
  };
69
+ var lastGeneratedHistoryId = 0n;
69
70
  function generateUid(prefix = "") {
70
71
  const id = randomBytes(12).toString("base64url").slice(0, 20);
71
72
  return prefix ? `${prefix}_${id}` : id;
@@ -76,7 +77,12 @@ function generateDraftId() {
76
77
  }
77
78
  function generateHistoryId() {
78
79
  const entropy = randomBytes(3).readUIntBE(0, 3).toString().padStart(8, "0");
79
- return `${Date.now()}${entropy}`;
80
+ let next = BigInt(`${Date.now()}${entropy}`);
81
+ if (next <= lastGeneratedHistoryId) {
82
+ next = lastGeneratedHistoryId + 1n;
83
+ }
84
+ lastGeneratedHistoryId = next;
85
+ return next.toString();
80
86
  }
81
87
  function getAuthenticatedEmail(c) {
82
88
  const authUser = c.get("authUser");
@@ -124,9 +130,7 @@ function parseBooleanParam(value) {
124
130
  return value === "true" || value === "1";
125
131
  }
126
132
  function ensureSystemLabels(gs, userEmail) {
127
- const existingIds = new Set(
128
- gs.labels.findBy("user_email", userEmail).map((row) => row.gmail_id)
129
- );
133
+ const existingIds = new Set(gs.labels.findBy("user_email", userEmail).map((row) => row.gmail_id));
130
134
  for (const label of SYSTEM_LABELS) {
131
135
  if (existingIds.has(label.gmail_id)) continue;
132
136
  gs.labels.insert({
@@ -471,9 +475,7 @@ function getCurrentHistoryId(gs, userEmail) {
471
475
  ...gs.history.findBy("user_email", userEmail).map((event) => event.gmail_id)
472
476
  ].filter(Boolean);
473
477
  if (historyIds.length === 0) return "0";
474
- return historyIds.reduce(
475
- (latest, current) => compareHistoryIds(current, latest) > 0 ? current : latest
476
- );
478
+ return historyIds.reduce((latest, current) => compareHistoryIds(current, latest) > 0 ? current : latest);
477
479
  }
478
480
  function listHistoryForUser(gs, userEmail, options) {
479
481
  const requestedTypes = options.historyTypes?.length ? new Set(options.historyTypes) : null;
@@ -1056,10 +1058,7 @@ function buildPayload(gs, message, headers, format) {
1056
1058
  filename: "",
1057
1059
  headers,
1058
1060
  body: { size: 0 },
1059
- parts: [
1060
- createTextBodyPart("0", "text/plain", textBody),
1061
- createTextBodyPart("1", "text/html", htmlBody)
1062
- ]
1061
+ parts: [createTextBodyPart("0", "text/plain", textBody), createTextBodyPart("1", "text/html", htmlBody)]
1063
1062
  };
1064
1063
  }
1065
1064
  if (htmlBody) return createTextBodyPart("", "text/html", htmlBody, headers);
@@ -1080,10 +1079,7 @@ function buildPayload(gs, message, headers, format) {
1080
1079
  filename: "",
1081
1080
  headers: [],
1082
1081
  body: { size: 0 },
1083
- parts: [
1084
- createTextBodyPart("0.0", "text/plain", textBody),
1085
- createTextBodyPart("0.1", "text/html", htmlBody)
1086
- ]
1082
+ parts: [createTextBodyPart("0.0", "text/plain", textBody), createTextBodyPart("0.1", "text/html", htmlBody)]
1087
1083
  });
1088
1084
  } else if (htmlBody) {
1089
1085
  parts.push(createTextBodyPart("0", "text/html", htmlBody));
@@ -1181,18 +1177,10 @@ function buildMimeBodyPart(input) {
1181
1177
  ].join("\r\n");
1182
1178
  }
1183
1179
  if (input.body_html) {
1184
- return [
1185
- "Content-Type: text/html; charset=utf-8",
1186
- "",
1187
- input.body_html
1188
- ].join("\r\n");
1180
+ return ["Content-Type: text/html; charset=utf-8", "", input.body_html].join("\r\n");
1189
1181
  }
1190
1182
  if (input.body_text) {
1191
- return [
1192
- "Content-Type: text/plain; charset=utf-8",
1193
- "",
1194
- input.body_text
1195
- ].join("\r\n");
1183
+ return ["Content-Type: text/plain; charset=utf-8", "", input.body_text].join("\r\n");
1196
1184
  }
1197
1185
  return null;
1198
1186
  }
@@ -1794,7 +1782,7 @@ function requireGmailUser(c) {
1794
1782
  if (authEmail instanceof Response) {
1795
1783
  return authEmail;
1796
1784
  }
1797
- if (!matchesRequestedUser(c.req.param("userId"), authEmail)) {
1785
+ if (!matchesRequestedUser(c.req.param("userId") ?? "", authEmail)) {
1798
1786
  return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
1799
1787
  }
1800
1788
  return authEmail;
@@ -1926,14 +1914,25 @@ function getGoogleStore(store) {
1926
1914
  oauthClients: store.collection("google.oauth_clients", ["client_id"]),
1927
1915
  messages: store.collection("google.messages", ["gmail_id", "thread_id", "user_email"]),
1928
1916
  drafts: store.collection("google.drafts", ["gmail_id", "message_gmail_id", "user_email"]),
1929
- attachments: store.collection("google.attachments", ["gmail_id", "message_gmail_id", "user_email"]),
1917
+ attachments: store.collection("google.attachments", [
1918
+ "gmail_id",
1919
+ "message_gmail_id",
1920
+ "user_email"
1921
+ ]),
1930
1922
  history: store.collection("google.history", ["gmail_id", "message_gmail_id", "user_email"]),
1931
1923
  labels: store.collection("google.labels", ["gmail_id", "user_email", "name"]),
1932
1924
  filters: store.collection("google.filters", ["gmail_id", "user_email"]),
1933
- forwardingAddresses: store.collection("google.forwarding_addresses", ["user_email", "forwarding_email"]),
1925
+ forwardingAddresses: store.collection("google.forwarding_addresses", [
1926
+ "user_email",
1927
+ "forwarding_email"
1928
+ ]),
1934
1929
  sendAs: store.collection("google.send_as", ["user_email", "send_as_email"]),
1935
1930
  calendars: store.collection("google.calendars", ["google_id", "user_email"]),
1936
- calendarEvents: store.collection("google.calendar_events", ["google_id", "calendar_google_id", "user_email"]),
1931
+ calendarEvents: store.collection("google.calendar_events", [
1932
+ "google_id",
1933
+ "calendar_google_id",
1934
+ "user_email"
1935
+ ]),
1937
1936
  driveItems: store.collection("google.drive_items", ["google_id", "user_email", "mime_type"])
1938
1937
  };
1939
1938
  }
@@ -2039,13 +2038,7 @@ function draftRoutes({ app, store }) {
2039
2038
  });
2040
2039
  return c.json(formatDraftResource(gs, draft, "full"));
2041
2040
  } catch {
2042
- return googleApiError(
2043
- c,
2044
- 400,
2045
- "Invalid raw MIME message payload.",
2046
- "invalidArgument",
2047
- "INVALID_ARGUMENT"
2048
- );
2041
+ return googleApiError(c, 400, "Invalid raw MIME message payload.", "invalidArgument", "INVALID_ARGUMENT");
2049
2042
  }
2050
2043
  };
2051
2044
  const sendHandler = async (c) => {
@@ -2135,13 +2128,7 @@ function draftRoutes({ app, store }) {
2135
2128
  }
2136
2129
  return c.json(formatDraftResource(gs, updated.draft, "full"));
2137
2130
  } catch {
2138
- return googleApiError(
2139
- c,
2140
- 400,
2141
- "Invalid raw MIME message payload.",
2142
- "invalidArgument",
2143
- "INVALID_ARGUMENT"
2144
- );
2131
+ return googleApiError(c, 400, "Invalid raw MIME message payload.", "invalidArgument", "INVALID_ARGUMENT");
2145
2132
  }
2146
2133
  });
2147
2134
  app.post("/gmail/v1/users/:userId/drafts/send", sendHandler);
@@ -2279,7 +2266,13 @@ function historyRoutes({ app, store }) {
2279
2266
  const labelIds = getStringArray(body, "labelIds");
2280
2267
  const missingLabelIds = findMissingLabelIds(gs, authEmail, labelIds);
2281
2268
  if (missingLabelIds.length > 0) {
2282
- return googleApiError(c, 400, `Invalid label IDs: ${missingLabelIds.join(", ")}`, "invalidArgument", "INVALID_ARGUMENT");
2269
+ return googleApiError(
2270
+ c,
2271
+ 400,
2272
+ `Invalid label IDs: ${missingLabelIds.join(", ")}`,
2273
+ "invalidArgument",
2274
+ "INVALID_ARGUMENT"
2275
+ );
2283
2276
  }
2284
2277
  const expiration = String(Date.now() + 24 * 60 * 60 * 1e3);
2285
2278
  const states = store.getData(WATCH_STATE_KEY) ?? /* @__PURE__ */ new Map();
@@ -2333,13 +2326,7 @@ function labelRoutes({ app, store }) {
2333
2326
  return googleApiError(c, 400, "Invalid label name", "invalidArgument", "INVALID_ARGUMENT");
2334
2327
  }
2335
2328
  if (findLabelByName(gs, authEmail, name)) {
2336
- return googleApiError(
2337
- c,
2338
- 400,
2339
- "Label name exists or conflicts",
2340
- "failedPrecondition",
2341
- "FAILED_PRECONDITION"
2342
- );
2329
+ return googleApiError(c, 400, "Label name exists or conflicts", "failedPrecondition", "FAILED_PRECONDITION");
2343
2330
  }
2344
2331
  const color = body.color && typeof body.color === "object" && !Array.isArray(body.color) ? body.color : void 0;
2345
2332
  const label = createLabelRecord(gs, {
@@ -2397,13 +2384,7 @@ async function saveLabel(c, gs, replaceMissingFields) {
2397
2384
  if (name) {
2398
2385
  const conflicting = findLabelByName(gs, authEmail, name);
2399
2386
  if (conflicting && conflicting.gmail_id !== label.gmail_id) {
2400
- return googleApiError(
2401
- c,
2402
- 400,
2403
- "Label name exists or conflicts",
2404
- "failedPrecondition",
2405
- "FAILED_PRECONDITION"
2406
- );
2387
+ return googleApiError(c, 400, "Label name exists or conflicts", "failedPrecondition", "FAILED_PRECONDITION");
2407
2388
  }
2408
2389
  }
2409
2390
  const updated = updateLabelRecord(gs, label, {
@@ -2427,7 +2408,13 @@ function messageRoutes({ app, store }) {
2427
2408
  const defaultLabelIds = mode === "send" ? dedupeLabelIds([...labelIds, "SENT"]) : labelIds.length > 0 ? labelIds : mode === "import" ? ["INBOX", "UNREAD"] : [];
2428
2409
  const missingLabelIds = findMissingLabelIds(gs, authEmail, defaultLabelIds);
2429
2410
  if (missingLabelIds.length > 0) {
2430
- return googleApiError(c, 400, `Invalid label IDs: ${missingLabelIds.join(", ")}`, "invalidArgument", "INVALID_ARGUMENT");
2411
+ return googleApiError(
2412
+ c,
2413
+ 400,
2414
+ `Invalid label IDs: ${missingLabelIds.join(", ")}`,
2415
+ "invalidArgument",
2416
+ "INVALID_ARGUMENT"
2417
+ );
2431
2418
  }
2432
2419
  const messageInput = parseMessageInputFromBody(body, {
2433
2420
  from: mode === "send" ? authEmail : void 0
@@ -2449,13 +2436,7 @@ function messageRoutes({ app, store }) {
2449
2436
  });
2450
2437
  return c.json(formatMessageResource(gs, message, "full"));
2451
2438
  } catch {
2452
- return googleApiError(
2453
- c,
2454
- 400,
2455
- "Invalid raw MIME message payload.",
2456
- "invalidArgument",
2457
- "INVALID_ARGUMENT"
2458
- );
2439
+ return googleApiError(c, 400, "Invalid raw MIME message payload.", "invalidArgument", "INVALID_ARGUMENT");
2459
2440
  }
2460
2441
  };
2461
2442
  app.get("/gmail/v1/users/:userId/messages", (c) => {
@@ -2489,16 +2470,18 @@ function messageRoutes({ app, store }) {
2489
2470
  const removeLabelIds = getStringArray(body, "removeLabelIds");
2490
2471
  const missingLabelIds = findMissingLabelIds(gs, authEmail, [...addLabelIds, ...removeLabelIds]);
2491
2472
  if (missingLabelIds.length > 0) {
2492
- return googleApiError(c, 400, `Invalid label IDs: ${missingLabelIds.join(", ")}`, "invalidArgument", "INVALID_ARGUMENT");
2473
+ return googleApiError(
2474
+ c,
2475
+ 400,
2476
+ `Invalid label IDs: ${missingLabelIds.join(", ")}`,
2477
+ "invalidArgument",
2478
+ "INVALID_ARGUMENT"
2479
+ );
2493
2480
  }
2494
2481
  for (const messageId of ids) {
2495
2482
  const message = getMessageById(gs, authEmail, messageId);
2496
2483
  if (!message) continue;
2497
- markMessageModified(
2498
- gs,
2499
- message,
2500
- applyLabelMutation(message.label_ids, addLabelIds, removeLabelIds)
2501
- );
2484
+ markMessageModified(gs, message, applyLabelMutation(message.label_ids, addLabelIds, removeLabelIds));
2502
2485
  }
2503
2486
  return c.body(null, 204);
2504
2487
  });
@@ -2565,7 +2548,13 @@ function messageRoutes({ app, store }) {
2565
2548
  const removeLabelIds = getStringArray(body, "removeLabelIds");
2566
2549
  const missingLabelIds = findMissingLabelIds(gs, authEmail, [...addLabelIds, ...removeLabelIds]);
2567
2550
  if (missingLabelIds.length > 0) {
2568
- return googleApiError(c, 400, `Invalid label IDs: ${missingLabelIds.join(", ")}`, "invalidArgument", "INVALID_ARGUMENT");
2551
+ return googleApiError(
2552
+ c,
2553
+ 400,
2554
+ `Invalid label IDs: ${missingLabelIds.join(", ")}`,
2555
+ "invalidArgument",
2556
+ "INVALID_ARGUMENT"
2557
+ );
2569
2558
  }
2570
2559
  const updated = markMessageModified(
2571
2560
  gs,
@@ -2581,7 +2570,9 @@ function messageRoutes({ app, store }) {
2581
2570
  if (!message) {
2582
2571
  return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2583
2572
  }
2584
- return c.json(formatMessageResource(gs, markMessageModified(gs, message, trashLabelIds(message.label_ids)), "full"));
2573
+ return c.json(
2574
+ formatMessageResource(gs, markMessageModified(gs, message, trashLabelIds(message.label_ids)), "full")
2575
+ );
2585
2576
  });
2586
2577
  app.post("/gmail/v1/users/:userId/messages/:id/untrash", (c) => {
2587
2578
  const authEmail = requireGmailUser(c);
@@ -2590,7 +2581,9 @@ function messageRoutes({ app, store }) {
2590
2581
  if (!message) {
2591
2582
  return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2592
2583
  }
2593
- return c.json(formatMessageResource(gs, markMessageModified(gs, message, untrashLabelIds(message.label_ids)), "full"));
2584
+ return c.json(
2585
+ formatMessageResource(gs, markMessageModified(gs, message, untrashLabelIds(message.label_ids)), "full")
2586
+ );
2594
2587
  });
2595
2588
  app.delete("/gmail/v1/users/:userId/messages/:id", (c) => {
2596
2589
  const authEmail = requireGmailUser(c);
@@ -2609,8 +2602,6 @@ import { createHash, randomBytes as randomBytes2 } from "crypto";
2609
2602
  import { SignJWT } from "jose";
2610
2603
 
2611
2604
  // ../core/dist/index.js
2612
- import { Hono } from "hono";
2613
- import { cors } from "hono/cors";
2614
2605
  import { jwtVerify, importPKCS8 } from "jose";
2615
2606
  import { readFileSync } from "fs";
2616
2607
  import { fileURLToPath } from "url";
@@ -2636,6 +2627,7 @@ var FONTS = {
2636
2627
  "geist-sans.woff2": readFileSync(join(__dirname, "fonts", "geist-sans.woff2")),
2637
2628
  "GeistPixel-Square.woff2": readFileSync(join(__dirname, "fonts", "GeistPixel-Square.woff2"))
2638
2629
  };
2630
+ var FAVICON = readFileSync(join(__dirname, "fonts", "favicon.ico"));
2639
2631
  function escapeHtml(s) {
2640
2632
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2641
2633
  }
@@ -2787,6 +2779,132 @@ body{
2787
2779
  .app-link-name{font-weight:600;font-size:.875rem;color:#33ff00;}
2788
2780
  .app-link-scopes{font-size:.6875rem;color:#1a8c00;margin-top:1px;}
2789
2781
  .empty{color:#1a8c00;text-align:center;padding:28px 0;font-size:.875rem;}
2782
+
2783
+ .inspector-layout{max-width:960px;margin:0 auto;padding:28px 20px;}
2784
+ .inspector-tabs{display:flex;gap:4px;margin-bottom:20px;}
2785
+ .inspector-tabs a{
2786
+ padding:7px 16px;border-radius:6px;text-decoration:none;
2787
+ font-size:.8125rem;color:#1a8c00;border:1px solid transparent;
2788
+ transition:color .15s,border-color .15s;
2789
+ }
2790
+ .inspector-tabs a:hover{color:#33ff00;}
2791
+ .inspector-tabs a.active{color:#33ff00;font-weight:600;border-color:#0a3300;background:#0a3300;}
2792
+ .inspector-section{margin-bottom:24px;}
2793
+ .inspector-section h2{
2794
+ font-family:'Geist Pixel',monospace;
2795
+ font-size:1rem;font-weight:600;color:#33ff00;margin-bottom:10px;
2796
+ }
2797
+ .inspector-section h3{
2798
+ font-family:'Geist Pixel',monospace;
2799
+ font-size:.875rem;font-weight:600;color:#1a8c00;margin:16px 0 8px;
2800
+ }
2801
+ .inspector-table{width:100%;border-collapse:collapse;margin-bottom:12px;}
2802
+ .inspector-table th,.inspector-table td{
2803
+ text-align:left;padding:8px 12px;border-bottom:1px solid #0a3300;
2804
+ font-size:.8125rem;
2805
+ }
2806
+ .inspector-table th{color:#1a8c00;font-weight:600;font-size:.75rem;text-transform:uppercase;letter-spacing:.04em;}
2807
+ .inspector-table td{color:#33ff00;}
2808
+ .inspector-table tbody tr{transition:background .1s;}
2809
+ .inspector-table tbody tr:hover{background:#0a3300;}
2810
+ .inspector-empty{color:#1a8c00;text-align:center;padding:20px 0;font-size:.8125rem;}
2811
+
2812
+ .checkout-layout{
2813
+ display:flex;min-height:calc(100vh - 42px);
2814
+ }
2815
+ .checkout-summary{
2816
+ flex:1;background:#020;padding:48px 40px 48px 10%;
2817
+ display:flex;flex-direction:column;justify-content:center;
2818
+ border-right:1px solid #0a3300;
2819
+ }
2820
+ .checkout-form-side{
2821
+ flex:1;background:#000;padding:48px 10% 48px 40px;
2822
+ display:flex;flex-direction:column;justify-content:center;
2823
+ }
2824
+ .checkout-merchant{
2825
+ display:flex;align-items:center;gap:10px;margin-bottom:6px;
2826
+ }
2827
+ .checkout-merchant-name{
2828
+ font-family:'Geist Pixel',monospace;
2829
+ font-size:.9375rem;font-weight:600;color:#33ff00;
2830
+ }
2831
+ .checkout-test-badge{
2832
+ font-size:.625rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
2833
+ background:#0a3300;color:#1a8c00;padding:2px 8px;border-radius:4px;
2834
+ }
2835
+ .checkout-total{
2836
+ font-family:'Geist Pixel',monospace;
2837
+ font-size:2rem;font-weight:700;color:#33ff00;margin:8px 0 28px;
2838
+ }
2839
+ .checkout-line-item{
2840
+ display:flex;align-items:center;gap:14px;padding:14px 0;
2841
+ border-bottom:1px solid #0a3300;
2842
+ }
2843
+ .checkout-line-item:first-child{border-top:1px solid #0a3300;}
2844
+ .checkout-item-icon{
2845
+ width:42px;height:42px;border-radius:6px;background:#0a3300;
2846
+ display:flex;align-items:center;justify-content:center;flex-shrink:0;
2847
+ font-family:'Geist Pixel',monospace;font-size:.875rem;font-weight:700;color:#116600;
2848
+ }
2849
+ .checkout-item-details{flex:1;min-width:0;}
2850
+ .checkout-item-name{font-size:.875rem;font-weight:600;color:#33ff00;}
2851
+ .checkout-item-qty{font-size:.75rem;color:#1a8c00;margin-top:2px;}
2852
+ .checkout-item-price{
2853
+ font-size:.875rem;font-weight:600;color:#33ff00;text-align:right;white-space:nowrap;
2854
+ }
2855
+ .checkout-item-unit{font-size:.6875rem;color:#1a8c00;text-align:right;margin-top:2px;}
2856
+ .checkout-totals{margin-top:20px;}
2857
+ .checkout-totals-row{
2858
+ display:flex;justify-content:space-between;padding:6px 0;
2859
+ font-size:.8125rem;color:#1a8c00;
2860
+ }
2861
+ .checkout-totals-row.total{
2862
+ border-top:1px solid #0a3300;margin-top:8px;padding-top:14px;
2863
+ font-size:.9375rem;font-weight:600;color:#33ff00;
2864
+ }
2865
+ .checkout-form-section{margin-bottom:24px;}
2866
+ .checkout-form-label{
2867
+ font-size:.8125rem;font-weight:600;color:#33ff00;margin-bottom:8px;display:block;
2868
+ }
2869
+ .checkout-input{
2870
+ width:100%;padding:10px 12px;border:1px solid #0a3300;border-radius:6px;
2871
+ background:#020;color:#33ff00;font:inherit;font-size:.875rem;
2872
+ transition:border-color .15s;outline:none;
2873
+ }
2874
+ .checkout-input:focus{border-color:#33ff00;}
2875
+ .checkout-input::placeholder{color:#116600;}
2876
+ .checkout-card-box{
2877
+ border:1px solid #0a3300;border-radius:6px;padding:14px;
2878
+ background:#020;
2879
+ }
2880
+ .checkout-card-row{
2881
+ display:flex;gap:12px;margin-top:10px;
2882
+ }
2883
+ .checkout-card-row .checkout-input{flex:1;}
2884
+ .checkout-sim-note{
2885
+ font-size:.6875rem;color:#1a8c00;margin-top:10px;text-align:center;
2886
+ font-style:italic;
2887
+ }
2888
+ .checkout-pay-btn{
2889
+ width:100%;padding:14px;border:none;border-radius:8px;
2890
+ background:#33ff00;color:#000;font:inherit;font-size:.9375rem;font-weight:700;
2891
+ cursor:pointer;transition:background .15s;
2892
+ font-family:'Geist Pixel',monospace;
2893
+ }
2894
+ .checkout-pay-btn:hover{background:#44ff22;}
2895
+ .checkout-cancel{
2896
+ text-align:center;margin-top:14px;
2897
+ }
2898
+ .checkout-cancel a{
2899
+ color:#1a8c00;text-decoration:none;font-size:.8125rem;
2900
+ transition:color .15s;
2901
+ }
2902
+ .checkout-cancel a:hover{color:#33ff00;}
2903
+ @media(max-width:768px){
2904
+ .checkout-layout{flex-direction:column;}
2905
+ .checkout-summary{padding:32px 20px;border-right:none;border-bottom:1px solid #0a3300;}
2906
+ .checkout-form-side{padding:32px 20px;}
2907
+ }
2790
2908
  `;
2791
2909
  var POWERED_BY = `<div class="powered-by">Powered by <a href="https://emulate.dev" target="_blank" rel="noopener">emulate</a></div>`;
2792
2910
  function emuBar(service) {
@@ -2806,6 +2924,7 @@ function head(title) {
2806
2924
  <head>
2807
2925
  <meta charset="utf-8"/>
2808
2926
  <meta name="viewport" content="width=device-width,initial-scale=1"/>
2927
+ <link rel="icon" href="/_emulate/favicon.ico"/>
2809
2928
  <title>${escapeHtml(title)} | emulate</title>
2810
2929
  <style>${CSS}</style>
2811
2930
  </head>`;
@@ -2909,6 +3028,7 @@ async function createIdToken(user, clientId, nonce, baseUrl) {
2909
3028
  family_name: user.family_name,
2910
3029
  picture: user.picture,
2911
3030
  locale: user.locale,
3031
+ ...user.hd ? { hd: user.hd } : {},
2912
3032
  ...nonce ? { nonce } : {}
2913
3033
  }).setProtectedHeader({ alg: "HS256", typ: "JWT" }).setIssuer(baseUrl).setAudience(clientId).setIssuedAt().setExpirationTime("1h");
2914
3034
  return builder.sign(JWT_SECRET);
@@ -2936,7 +3056,8 @@ function oauthRoutes({ app, store, baseUrl, tokenMap }) {
2936
3056
  "given_name",
2937
3057
  "family_name",
2938
3058
  "picture",
2939
- "locale"
3059
+ "locale",
3060
+ "hd"
2940
3061
  ],
2941
3062
  code_challenge_methods_supported: ["plain", "S256"]
2942
3063
  });
@@ -2964,7 +3085,11 @@ function oauthRoutes({ app, store, baseUrl, tokenMap }) {
2964
3085
  }
2965
3086
  if (redirect_uri && !matchesRedirectUri(redirect_uri, client.redirect_uris)) {
2966
3087
  return c.html(
2967
- renderErrorPage("Redirect URI mismatch", "The redirect_uri is not registered for this application.", SERVICE_LABEL),
3088
+ renderErrorPage(
3089
+ "Redirect URI mismatch",
3090
+ "The redirect_uri is not registered for this application.",
3091
+ SERVICE_LABEL
3092
+ ),
2968
3093
  400
2969
3094
  );
2970
3095
  }
@@ -3076,7 +3201,13 @@ function oauthRoutes({ app, store, baseUrl, tokenMap }) {
3076
3201
  });
3077
3202
  }
3078
3203
  if (grant_type !== "authorization_code") {
3079
- return c.json({ error: "unsupported_grant_type", error_description: "Only authorization_code and refresh_token are supported." }, 400);
3204
+ return c.json(
3205
+ {
3206
+ error: "unsupported_grant_type",
3207
+ error_description: "Only authorization_code and refresh_token are supported."
3208
+ },
3209
+ 400
3210
+ );
3080
3211
  }
3081
3212
  const pendingMap = getPendingCodes(store);
3082
3213
  const pending = pendingMap.get(code);
@@ -3149,7 +3280,8 @@ function oauthRoutes({ app, store, baseUrl, tokenMap }) {
3149
3280
  given_name: user.given_name,
3150
3281
  family_name: user.family_name,
3151
3282
  picture: user.picture,
3152
- locale: user.locale
3283
+ locale: user.locale,
3284
+ ...user.hd ? { hd: user.hd } : {}
3153
3285
  });
3154
3286
  });
3155
3287
  app.post("/oauth2/revoke", async (c) => {
@@ -3201,7 +3333,13 @@ function settingsRoutes({ app, store }) {
3201
3333
  }
3202
3334
  const missingLabelIds = findMissingLabelIds(gs, authEmail, [...addLabelIds, ...removeLabelIds]);
3203
3335
  if (missingLabelIds.length > 0) {
3204
- return googleApiError(c, 400, `Invalid label IDs: ${missingLabelIds.join(", ")}`, "invalidArgument", "INVALID_ARGUMENT");
3336
+ return googleApiError(
3337
+ c,
3338
+ 400,
3339
+ `Invalid label IDs: ${missingLabelIds.join(", ")}`,
3340
+ "invalidArgument",
3341
+ "INVALID_ARGUMENT"
3342
+ );
3205
3343
  }
3206
3344
  if (findMatchingFilter(gs, {
3207
3345
  user_email: authEmail,
@@ -3306,14 +3444,16 @@ function threadRoutes({ app, store }) {
3306
3444
  const removeLabelIds = getStringArray(body, "removeLabelIds");
3307
3445
  const missingLabelIds = findMissingLabelIds(gs, authEmail, [...addLabelIds, ...removeLabelIds]);
3308
3446
  if (missingLabelIds.length > 0) {
3309
- return googleApiError(c, 400, `Invalid label IDs: ${missingLabelIds.join(", ")}`, "invalidArgument", "INVALID_ARGUMENT");
3447
+ return googleApiError(
3448
+ c,
3449
+ 400,
3450
+ `Invalid label IDs: ${missingLabelIds.join(", ")}`,
3451
+ "invalidArgument",
3452
+ "INVALID_ARGUMENT"
3453
+ );
3310
3454
  }
3311
3455
  const updated = messages.map(
3312
- (message) => markMessageModified(
3313
- gs,
3314
- message,
3315
- applyLabelMutation(message.label_ids, addLabelIds, removeLabelIds)
3316
- )
3456
+ (message) => markMessageModified(gs, message, applyLabelMutation(message.label_ids, addLabelIds, removeLabelIds))
3317
3457
  );
3318
3458
  return c.json(formatThreadResource(gs, updated, "full"));
3319
3459
  });
@@ -3364,120 +3504,148 @@ function seedDefaults(store, _baseUrl) {
3364
3504
  family_name: "User",
3365
3505
  picture: null,
3366
3506
  email_verified: true,
3367
- locale: "en"
3507
+ locale: "en",
3508
+ hd: null
3368
3509
  });
3369
3510
  }
3370
3511
  ensureSystemLabels(gs, defaultEmail);
3371
- seedCalendars(store, [
3372
- {
3373
- id: "primary",
3374
- user_email: defaultEmail,
3375
- summary: defaultEmail,
3376
- primary: true,
3377
- selected: true,
3378
- time_zone: "UTC"
3379
- },
3380
- {
3381
- id: "cal_team",
3382
- user_email: defaultEmail,
3383
- summary: "Team Calendar",
3384
- description: "Shared team events",
3385
- selected: true,
3386
- time_zone: "UTC"
3387
- }
3388
- ], defaultEmail);
3389
- seedCalendarEvents(store, [
3390
- {
3391
- id: "evt_standup",
3392
- user_email: defaultEmail,
3393
- calendar_id: "primary",
3394
- summary: "Daily Standup",
3395
- description: "Team sync",
3396
- start_date_time: new Date(Date.now() + 60 * 60 * 1e3).toISOString(),
3397
- end_date_time: new Date(Date.now() + 90 * 60 * 1e3).toISOString(),
3398
- attendees: [
3399
- { email: defaultEmail, display_name: "Test User" },
3400
- { email: "teammate@example.com", display_name: "Teammate" }
3401
- ],
3402
- conference_entry_points: [
3403
- {
3404
- entry_point_type: "video",
3405
- uri: "https://meet.google.com/emulate-standup",
3406
- label: "Google Meet"
3407
- }
3408
- ],
3409
- hangout_link: "https://meet.google.com/emulate-standup"
3410
- }
3411
- ], defaultEmail);
3412
- seedDriveItems(store, [
3413
- {
3414
- id: "drv_root_receipts",
3415
- user_email: defaultEmail,
3416
- name: "Receipts",
3417
- mime_type: "application/vnd.google-apps.folder",
3418
- parent_ids: ["root"]
3419
- },
3420
- {
3421
- id: "drv_receipt_pdf",
3422
- user_email: defaultEmail,
3423
- name: "March Receipt.pdf",
3424
- mime_type: "application/pdf",
3425
- parent_ids: ["drv_root_receipts"],
3426
- data: "receipt-pdf-data"
3427
- }
3428
- ], defaultEmail);
3429
- seedMessages(store, [
3430
- {
3431
- id: "msg_welcome",
3432
- thread_id: "thr_welcome",
3433
- user_email: defaultEmail,
3434
- from: "Welcome Team <welcome@example.com>",
3435
- to: defaultEmail,
3436
- subject: "Welcome to your local Gmail emulator",
3437
- snippet: "Your OAuth flow is set up and Gmail message, thread, and label APIs are ready.",
3438
- body_text: "Your OAuth flow is set up and Gmail message, thread, and label APIs are ready.\n\nUse this inbox to test Gmail automations locally.",
3439
- label_ids: ["INBOX", "UNREAD", "CATEGORY_UPDATES"],
3440
- date: new Date(Date.now() - 60 * 60 * 1e3).toISOString()
3441
- },
3442
- {
3443
- id: "msg_build",
3444
- thread_id: "thr_build",
3445
- user_email: defaultEmail,
3446
- from: "Build Bot <builds@example.com>",
3447
- to: defaultEmail,
3448
- subject: "Nightly build finished successfully",
3449
- snippet: "The latest build completed successfully in 6 minutes.",
3450
- body_text: "The latest build completed successfully in 6 minutes.\n\nArtifact upload finished and smoke checks passed.",
3451
- label_ids: ["INBOX", "CATEGORY_UPDATES"],
3452
- date: new Date(Date.now() - 2 * 60 * 60 * 1e3).toISOString()
3453
- },
3454
- {
3455
- id: "msg_build_reply",
3456
- thread_id: "thr_build",
3457
- user_email: defaultEmail,
3458
- from: defaultEmail,
3459
- to: "Build Bot <builds@example.com>",
3460
- subject: "Re: Nightly build finished successfully",
3461
- snippet: "Thanks, I will review the artifact after lunch.",
3462
- body_text: "Thanks, I will review the artifact after lunch.",
3463
- label_ids: ["SENT"],
3464
- date: new Date(Date.now() - 90 * 60 * 1e3).toISOString(),
3465
- in_reply_to: "<msg_build@emulate.google.local>",
3466
- references: "<msg_build@emulate.google.local>"
3467
- },
3468
- {
3469
- id: "msg_draft",
3470
- thread_id: "thr_draft",
3471
- user_email: defaultEmail,
3472
- from: defaultEmail,
3473
- to: "someone@example.com",
3474
- subject: "Draft follow-up",
3475
- snippet: "Checking in on the open question from yesterday.",
3476
- body_text: "Checking in on the open question from yesterday.",
3477
- label_ids: ["DRAFT"],
3478
- date: new Date(Date.now() - 30 * 60 * 1e3).toISOString()
3479
- }
3480
- ], defaultEmail);
3512
+ seedCalendars(
3513
+ store,
3514
+ [
3515
+ {
3516
+ id: "primary",
3517
+ user_email: defaultEmail,
3518
+ summary: defaultEmail,
3519
+ primary: true,
3520
+ selected: true,
3521
+ time_zone: "UTC"
3522
+ },
3523
+ {
3524
+ id: "cal_team",
3525
+ user_email: defaultEmail,
3526
+ summary: "Team Calendar",
3527
+ description: "Shared team events",
3528
+ selected: true,
3529
+ time_zone: "UTC"
3530
+ }
3531
+ ],
3532
+ defaultEmail
3533
+ );
3534
+ seedCalendarEvents(
3535
+ store,
3536
+ [
3537
+ {
3538
+ id: "evt_standup",
3539
+ user_email: defaultEmail,
3540
+ calendar_id: "primary",
3541
+ summary: "Daily Standup",
3542
+ description: "Team sync",
3543
+ start_date_time: new Date(Date.now() + 60 * 60 * 1e3).toISOString(),
3544
+ end_date_time: new Date(Date.now() + 90 * 60 * 1e3).toISOString(),
3545
+ attendees: [
3546
+ { email: defaultEmail, display_name: "Test User" },
3547
+ { email: "teammate@example.com", display_name: "Teammate" }
3548
+ ],
3549
+ conference_entry_points: [
3550
+ {
3551
+ entry_point_type: "video",
3552
+ uri: "https://meet.google.com/emulate-standup",
3553
+ label: "Google Meet"
3554
+ }
3555
+ ],
3556
+ hangout_link: "https://meet.google.com/emulate-standup"
3557
+ }
3558
+ ],
3559
+ defaultEmail
3560
+ );
3561
+ seedDriveItems(
3562
+ store,
3563
+ [
3564
+ {
3565
+ id: "drv_root_receipts",
3566
+ user_email: defaultEmail,
3567
+ name: "Receipts",
3568
+ mime_type: "application/vnd.google-apps.folder",
3569
+ parent_ids: ["root"]
3570
+ },
3571
+ {
3572
+ id: "drv_receipt_pdf",
3573
+ user_email: defaultEmail,
3574
+ name: "March Receipt.pdf",
3575
+ mime_type: "application/pdf",
3576
+ parent_ids: ["drv_root_receipts"],
3577
+ data: "receipt-pdf-data"
3578
+ }
3579
+ ],
3580
+ defaultEmail
3581
+ );
3582
+ seedMessages(
3583
+ store,
3584
+ [
3585
+ {
3586
+ id: "msg_welcome",
3587
+ thread_id: "thr_welcome",
3588
+ user_email: defaultEmail,
3589
+ from: "Welcome Team <welcome@example.com>",
3590
+ to: defaultEmail,
3591
+ subject: "Welcome to your local Gmail emulator",
3592
+ snippet: "Your OAuth flow is set up and Gmail message, thread, and label APIs are ready.",
3593
+ body_text: "Your OAuth flow is set up and Gmail message, thread, and label APIs are ready.\n\nUse this inbox to test Gmail automations locally.",
3594
+ label_ids: ["INBOX", "UNREAD", "CATEGORY_UPDATES"],
3595
+ date: new Date(Date.now() - 60 * 60 * 1e3).toISOString()
3596
+ },
3597
+ {
3598
+ id: "msg_build",
3599
+ thread_id: "thr_build",
3600
+ user_email: defaultEmail,
3601
+ from: "Build Bot <builds@example.com>",
3602
+ to: defaultEmail,
3603
+ subject: "Nightly build finished successfully",
3604
+ snippet: "The latest build completed successfully in 6 minutes.",
3605
+ body_text: "The latest build completed successfully in 6 minutes.\n\nArtifact upload finished and smoke checks passed.",
3606
+ label_ids: ["INBOX", "CATEGORY_UPDATES"],
3607
+ date: new Date(Date.now() - 2 * 60 * 60 * 1e3).toISOString()
3608
+ },
3609
+ {
3610
+ id: "msg_build_reply",
3611
+ thread_id: "thr_build",
3612
+ user_email: defaultEmail,
3613
+ from: defaultEmail,
3614
+ to: "Build Bot <builds@example.com>",
3615
+ subject: "Re: Nightly build finished successfully",
3616
+ snippet: "Thanks, I will review the artifact after lunch.",
3617
+ body_text: "Thanks, I will review the artifact after lunch.",
3618
+ label_ids: ["SENT"],
3619
+ date: new Date(Date.now() - 90 * 60 * 1e3).toISOString(),
3620
+ in_reply_to: "<msg_build@emulate.google.local>",
3621
+ references: "<msg_build@emulate.google.local>"
3622
+ },
3623
+ {
3624
+ id: "msg_draft",
3625
+ thread_id: "thr_draft",
3626
+ user_email: defaultEmail,
3627
+ from: defaultEmail,
3628
+ to: "someone@example.com",
3629
+ subject: "Draft follow-up",
3630
+ snippet: "Checking in on the open question from yesterday.",
3631
+ body_text: "Checking in on the open question from yesterday.",
3632
+ label_ids: ["DRAFT"],
3633
+ date: new Date(Date.now() - 30 * 60 * 1e3).toISOString()
3634
+ }
3635
+ ],
3636
+ defaultEmail
3637
+ );
3638
+ }
3639
+ var CONSUMER_EMAIL_DOMAINS = /* @__PURE__ */ new Set(["gmail.com", "googlemail.com"]);
3640
+ function deriveHd(email) {
3641
+ const domain = email.split("@")[1]?.toLowerCase();
3642
+ if (!domain) return null;
3643
+ if (CONSUMER_EMAIL_DOMAINS.has(domain)) return null;
3644
+ return domain;
3645
+ }
3646
+ function resolveHd(user) {
3647
+ if (user.hd !== void 0) return user.hd || null;
3648
+ return deriveHd(user.email);
3481
3649
  }
3482
3650
  function seedFromConfig(store, _baseUrl, config) {
3483
3651
  const gs = getGoogleStore(store);
@@ -3494,7 +3662,8 @@ function seedFromConfig(store, _baseUrl, config) {
3494
3662
  family_name: user.family_name ?? nameParts.slice(1).join(" "),
3495
3663
  picture: user.picture ?? null,
3496
3664
  email_verified: user.email_verified ?? true,
3497
- locale: user.locale ?? "en"
3665
+ locale: user.locale ?? "en",
3666
+ hd: resolveHd(user)
3498
3667
  });
3499
3668
  }
3500
3669
  ensureSystemLabels(gs, user.email);
@@ -3555,29 +3724,33 @@ function seedMessages(store, messages, fallbackEmail) {
3555
3724
  const userEmail = message.user_email ?? fallbackEmail;
3556
3725
  ensureSystemLabels(gs, userEmail);
3557
3726
  if (message.id && gs.messages.findOneBy("gmail_id", message.id)) continue;
3558
- createStoredMessage(gs, {
3559
- gmail_id: message.id,
3560
- thread_id: message.thread_id,
3561
- user_email: userEmail,
3562
- raw: message.raw ?? null,
3563
- from: message.from,
3564
- to: message.to,
3565
- cc: message.cc ?? null,
3566
- bcc: message.bcc ?? null,
3567
- reply_to: message.reply_to ?? null,
3568
- subject: message.subject,
3569
- snippet: message.snippet,
3570
- body_text: message.body_text ?? null,
3571
- body_html: message.body_html ?? null,
3572
- label_ids: message.label_ids ?? ["INBOX", "UNREAD"],
3573
- date: message.date,
3574
- internal_date: message.internal_date,
3575
- message_id: message.message_id,
3576
- references: message.references ?? null,
3577
- in_reply_to: message.in_reply_to ?? null
3578
- }, {
3579
- createMissingCustomLabels: true
3580
- });
3727
+ createStoredMessage(
3728
+ gs,
3729
+ {
3730
+ gmail_id: message.id,
3731
+ thread_id: message.thread_id,
3732
+ user_email: userEmail,
3733
+ raw: message.raw ?? null,
3734
+ from: message.from,
3735
+ to: message.to,
3736
+ cc: message.cc ?? null,
3737
+ bcc: message.bcc ?? null,
3738
+ reply_to: message.reply_to ?? null,
3739
+ subject: message.subject,
3740
+ snippet: message.snippet,
3741
+ body_text: message.body_text ?? null,
3742
+ body_html: message.body_html ?? null,
3743
+ label_ids: message.label_ids ?? ["INBOX", "UNREAD"],
3744
+ date: message.date,
3745
+ internal_date: message.internal_date,
3746
+ message_id: message.message_id,
3747
+ references: message.references ?? null,
3748
+ in_reply_to: message.in_reply_to ?? null
3749
+ },
3750
+ {
3751
+ createMissingCustomLabels: true
3752
+ }
3753
+ );
3581
3754
  }
3582
3755
  }
3583
3756
  function seedCalendars(store, calendars, fallbackEmail) {