@emulators/google 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -124,9 +124,7 @@ function parseBooleanParam(value) {
124
124
  return value === "true" || value === "1";
125
125
  }
126
126
  function ensureSystemLabels(gs, userEmail) {
127
- const existingIds = new Set(
128
- gs.labels.findBy("user_email", userEmail).map((row) => row.gmail_id)
129
- );
127
+ const existingIds = new Set(gs.labels.findBy("user_email", userEmail).map((row) => row.gmail_id));
130
128
  for (const label of SYSTEM_LABELS) {
131
129
  if (existingIds.has(label.gmail_id)) continue;
132
130
  gs.labels.insert({
@@ -471,9 +469,7 @@ function getCurrentHistoryId(gs, userEmail) {
471
469
  ...gs.history.findBy("user_email", userEmail).map((event) => event.gmail_id)
472
470
  ].filter(Boolean);
473
471
  if (historyIds.length === 0) return "0";
474
- return historyIds.reduce(
475
- (latest, current) => compareHistoryIds(current, latest) > 0 ? current : latest
476
- );
472
+ return historyIds.reduce((latest, current) => compareHistoryIds(current, latest) > 0 ? current : latest);
477
473
  }
478
474
  function listHistoryForUser(gs, userEmail, options) {
479
475
  const requestedTypes = options.historyTypes?.length ? new Set(options.historyTypes) : null;
@@ -1056,10 +1052,7 @@ function buildPayload(gs, message, headers, format) {
1056
1052
  filename: "",
1057
1053
  headers,
1058
1054
  body: { size: 0 },
1059
- parts: [
1060
- createTextBodyPart("0", "text/plain", textBody),
1061
- createTextBodyPart("1", "text/html", htmlBody)
1062
- ]
1055
+ parts: [createTextBodyPart("0", "text/plain", textBody), createTextBodyPart("1", "text/html", htmlBody)]
1063
1056
  };
1064
1057
  }
1065
1058
  if (htmlBody) return createTextBodyPart("", "text/html", htmlBody, headers);
@@ -1080,10 +1073,7 @@ function buildPayload(gs, message, headers, format) {
1080
1073
  filename: "",
1081
1074
  headers: [],
1082
1075
  body: { size: 0 },
1083
- parts: [
1084
- createTextBodyPart("0.0", "text/plain", textBody),
1085
- createTextBodyPart("0.1", "text/html", htmlBody)
1086
- ]
1076
+ parts: [createTextBodyPart("0.0", "text/plain", textBody), createTextBodyPart("0.1", "text/html", htmlBody)]
1087
1077
  });
1088
1078
  } else if (htmlBody) {
1089
1079
  parts.push(createTextBodyPart("0", "text/html", htmlBody));
@@ -1181,18 +1171,10 @@ function buildMimeBodyPart(input) {
1181
1171
  ].join("\r\n");
1182
1172
  }
1183
1173
  if (input.body_html) {
1184
- return [
1185
- "Content-Type: text/html; charset=utf-8",
1186
- "",
1187
- input.body_html
1188
- ].join("\r\n");
1174
+ return ["Content-Type: text/html; charset=utf-8", "", input.body_html].join("\r\n");
1189
1175
  }
1190
1176
  if (input.body_text) {
1191
- return [
1192
- "Content-Type: text/plain; charset=utf-8",
1193
- "",
1194
- input.body_text
1195
- ].join("\r\n");
1177
+ return ["Content-Type: text/plain; charset=utf-8", "", input.body_text].join("\r\n");
1196
1178
  }
1197
1179
  return null;
1198
1180
  }
@@ -1794,7 +1776,7 @@ function requireGmailUser(c) {
1794
1776
  if (authEmail instanceof Response) {
1795
1777
  return authEmail;
1796
1778
  }
1797
- if (!matchesRequestedUser(c.req.param("userId"), authEmail)) {
1779
+ if (!matchesRequestedUser(c.req.param("userId") ?? "", authEmail)) {
1798
1780
  return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
1799
1781
  }
1800
1782
  return authEmail;
@@ -1926,14 +1908,25 @@ function getGoogleStore(store) {
1926
1908
  oauthClients: store.collection("google.oauth_clients", ["client_id"]),
1927
1909
  messages: store.collection("google.messages", ["gmail_id", "thread_id", "user_email"]),
1928
1910
  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"]),
1911
+ attachments: store.collection("google.attachments", [
1912
+ "gmail_id",
1913
+ "message_gmail_id",
1914
+ "user_email"
1915
+ ]),
1930
1916
  history: store.collection("google.history", ["gmail_id", "message_gmail_id", "user_email"]),
1931
1917
  labels: store.collection("google.labels", ["gmail_id", "user_email", "name"]),
1932
1918
  filters: store.collection("google.filters", ["gmail_id", "user_email"]),
1933
- forwardingAddresses: store.collection("google.forwarding_addresses", ["user_email", "forwarding_email"]),
1919
+ forwardingAddresses: store.collection("google.forwarding_addresses", [
1920
+ "user_email",
1921
+ "forwarding_email"
1922
+ ]),
1934
1923
  sendAs: store.collection("google.send_as", ["user_email", "send_as_email"]),
1935
1924
  calendars: store.collection("google.calendars", ["google_id", "user_email"]),
1936
- calendarEvents: store.collection("google.calendar_events", ["google_id", "calendar_google_id", "user_email"]),
1925
+ calendarEvents: store.collection("google.calendar_events", [
1926
+ "google_id",
1927
+ "calendar_google_id",
1928
+ "user_email"
1929
+ ]),
1937
1930
  driveItems: store.collection("google.drive_items", ["google_id", "user_email", "mime_type"])
1938
1931
  };
1939
1932
  }
@@ -2039,13 +2032,7 @@ function draftRoutes({ app, store }) {
2039
2032
  });
2040
2033
  return c.json(formatDraftResource(gs, draft, "full"));
2041
2034
  } catch {
2042
- return googleApiError(
2043
- c,
2044
- 400,
2045
- "Invalid raw MIME message payload.",
2046
- "invalidArgument",
2047
- "INVALID_ARGUMENT"
2048
- );
2035
+ return googleApiError(c, 400, "Invalid raw MIME message payload.", "invalidArgument", "INVALID_ARGUMENT");
2049
2036
  }
2050
2037
  };
2051
2038
  const sendHandler = async (c) => {
@@ -2135,13 +2122,7 @@ function draftRoutes({ app, store }) {
2135
2122
  }
2136
2123
  return c.json(formatDraftResource(gs, updated.draft, "full"));
2137
2124
  } catch {
2138
- return googleApiError(
2139
- c,
2140
- 400,
2141
- "Invalid raw MIME message payload.",
2142
- "invalidArgument",
2143
- "INVALID_ARGUMENT"
2144
- );
2125
+ return googleApiError(c, 400, "Invalid raw MIME message payload.", "invalidArgument", "INVALID_ARGUMENT");
2145
2126
  }
2146
2127
  });
2147
2128
  app.post("/gmail/v1/users/:userId/drafts/send", sendHandler);
@@ -2279,7 +2260,13 @@ function historyRoutes({ app, store }) {
2279
2260
  const labelIds = getStringArray(body, "labelIds");
2280
2261
  const missingLabelIds = findMissingLabelIds(gs, authEmail, labelIds);
2281
2262
  if (missingLabelIds.length > 0) {
2282
- return googleApiError(c, 400, `Invalid label IDs: ${missingLabelIds.join(", ")}`, "invalidArgument", "INVALID_ARGUMENT");
2263
+ return googleApiError(
2264
+ c,
2265
+ 400,
2266
+ `Invalid label IDs: ${missingLabelIds.join(", ")}`,
2267
+ "invalidArgument",
2268
+ "INVALID_ARGUMENT"
2269
+ );
2283
2270
  }
2284
2271
  const expiration = String(Date.now() + 24 * 60 * 60 * 1e3);
2285
2272
  const states = store.getData(WATCH_STATE_KEY) ?? /* @__PURE__ */ new Map();
@@ -2333,13 +2320,7 @@ function labelRoutes({ app, store }) {
2333
2320
  return googleApiError(c, 400, "Invalid label name", "invalidArgument", "INVALID_ARGUMENT");
2334
2321
  }
2335
2322
  if (findLabelByName(gs, authEmail, name)) {
2336
- return googleApiError(
2337
- c,
2338
- 400,
2339
- "Label name exists or conflicts",
2340
- "failedPrecondition",
2341
- "FAILED_PRECONDITION"
2342
- );
2323
+ return googleApiError(c, 400, "Label name exists or conflicts", "failedPrecondition", "FAILED_PRECONDITION");
2343
2324
  }
2344
2325
  const color = body.color && typeof body.color === "object" && !Array.isArray(body.color) ? body.color : void 0;
2345
2326
  const label = createLabelRecord(gs, {
@@ -2397,13 +2378,7 @@ async function saveLabel(c, gs, replaceMissingFields) {
2397
2378
  if (name) {
2398
2379
  const conflicting = findLabelByName(gs, authEmail, name);
2399
2380
  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
- );
2381
+ return googleApiError(c, 400, "Label name exists or conflicts", "failedPrecondition", "FAILED_PRECONDITION");
2407
2382
  }
2408
2383
  }
2409
2384
  const updated = updateLabelRecord(gs, label, {
@@ -2427,7 +2402,13 @@ function messageRoutes({ app, store }) {
2427
2402
  const defaultLabelIds = mode === "send" ? dedupeLabelIds([...labelIds, "SENT"]) : labelIds.length > 0 ? labelIds : mode === "import" ? ["INBOX", "UNREAD"] : [];
2428
2403
  const missingLabelIds = findMissingLabelIds(gs, authEmail, defaultLabelIds);
2429
2404
  if (missingLabelIds.length > 0) {
2430
- return googleApiError(c, 400, `Invalid label IDs: ${missingLabelIds.join(", ")}`, "invalidArgument", "INVALID_ARGUMENT");
2405
+ return googleApiError(
2406
+ c,
2407
+ 400,
2408
+ `Invalid label IDs: ${missingLabelIds.join(", ")}`,
2409
+ "invalidArgument",
2410
+ "INVALID_ARGUMENT"
2411
+ );
2431
2412
  }
2432
2413
  const messageInput = parseMessageInputFromBody(body, {
2433
2414
  from: mode === "send" ? authEmail : void 0
@@ -2449,13 +2430,7 @@ function messageRoutes({ app, store }) {
2449
2430
  });
2450
2431
  return c.json(formatMessageResource(gs, message, "full"));
2451
2432
  } catch {
2452
- return googleApiError(
2453
- c,
2454
- 400,
2455
- "Invalid raw MIME message payload.",
2456
- "invalidArgument",
2457
- "INVALID_ARGUMENT"
2458
- );
2433
+ return googleApiError(c, 400, "Invalid raw MIME message payload.", "invalidArgument", "INVALID_ARGUMENT");
2459
2434
  }
2460
2435
  };
2461
2436
  app.get("/gmail/v1/users/:userId/messages", (c) => {
@@ -2489,16 +2464,18 @@ function messageRoutes({ app, store }) {
2489
2464
  const removeLabelIds = getStringArray(body, "removeLabelIds");
2490
2465
  const missingLabelIds = findMissingLabelIds(gs, authEmail, [...addLabelIds, ...removeLabelIds]);
2491
2466
  if (missingLabelIds.length > 0) {
2492
- return googleApiError(c, 400, `Invalid label IDs: ${missingLabelIds.join(", ")}`, "invalidArgument", "INVALID_ARGUMENT");
2467
+ return googleApiError(
2468
+ c,
2469
+ 400,
2470
+ `Invalid label IDs: ${missingLabelIds.join(", ")}`,
2471
+ "invalidArgument",
2472
+ "INVALID_ARGUMENT"
2473
+ );
2493
2474
  }
2494
2475
  for (const messageId of ids) {
2495
2476
  const message = getMessageById(gs, authEmail, messageId);
2496
2477
  if (!message) continue;
2497
- markMessageModified(
2498
- gs,
2499
- message,
2500
- applyLabelMutation(message.label_ids, addLabelIds, removeLabelIds)
2501
- );
2478
+ markMessageModified(gs, message, applyLabelMutation(message.label_ids, addLabelIds, removeLabelIds));
2502
2479
  }
2503
2480
  return c.body(null, 204);
2504
2481
  });
@@ -2565,7 +2542,13 @@ function messageRoutes({ app, store }) {
2565
2542
  const removeLabelIds = getStringArray(body, "removeLabelIds");
2566
2543
  const missingLabelIds = findMissingLabelIds(gs, authEmail, [...addLabelIds, ...removeLabelIds]);
2567
2544
  if (missingLabelIds.length > 0) {
2568
- return googleApiError(c, 400, `Invalid label IDs: ${missingLabelIds.join(", ")}`, "invalidArgument", "INVALID_ARGUMENT");
2545
+ return googleApiError(
2546
+ c,
2547
+ 400,
2548
+ `Invalid label IDs: ${missingLabelIds.join(", ")}`,
2549
+ "invalidArgument",
2550
+ "INVALID_ARGUMENT"
2551
+ );
2569
2552
  }
2570
2553
  const updated = markMessageModified(
2571
2554
  gs,
@@ -2581,7 +2564,9 @@ function messageRoutes({ app, store }) {
2581
2564
  if (!message) {
2582
2565
  return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2583
2566
  }
2584
- return c.json(formatMessageResource(gs, markMessageModified(gs, message, trashLabelIds(message.label_ids)), "full"));
2567
+ return c.json(
2568
+ formatMessageResource(gs, markMessageModified(gs, message, trashLabelIds(message.label_ids)), "full")
2569
+ );
2585
2570
  });
2586
2571
  app.post("/gmail/v1/users/:userId/messages/:id/untrash", (c) => {
2587
2572
  const authEmail = requireGmailUser(c);
@@ -2590,7 +2575,9 @@ function messageRoutes({ app, store }) {
2590
2575
  if (!message) {
2591
2576
  return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2592
2577
  }
2593
- return c.json(formatMessageResource(gs, markMessageModified(gs, message, untrashLabelIds(message.label_ids)), "full"));
2578
+ return c.json(
2579
+ formatMessageResource(gs, markMessageModified(gs, message, untrashLabelIds(message.label_ids)), "full")
2580
+ );
2594
2581
  });
2595
2582
  app.delete("/gmail/v1/users/:userId/messages/:id", (c) => {
2596
2583
  const authEmail = requireGmailUser(c);
@@ -2636,6 +2623,7 @@ var FONTS = {
2636
2623
  "geist-sans.woff2": readFileSync(join(__dirname, "fonts", "geist-sans.woff2")),
2637
2624
  "GeistPixel-Square.woff2": readFileSync(join(__dirname, "fonts", "GeistPixel-Square.woff2"))
2638
2625
  };
2626
+ var FAVICON = readFileSync(join(__dirname, "fonts", "favicon.ico"));
2639
2627
  function escapeHtml(s) {
2640
2628
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2641
2629
  }
@@ -2787,6 +2775,132 @@ body{
2787
2775
  .app-link-name{font-weight:600;font-size:.875rem;color:#33ff00;}
2788
2776
  .app-link-scopes{font-size:.6875rem;color:#1a8c00;margin-top:1px;}
2789
2777
  .empty{color:#1a8c00;text-align:center;padding:28px 0;font-size:.875rem;}
2778
+
2779
+ .inspector-layout{max-width:960px;margin:0 auto;padding:28px 20px;}
2780
+ .inspector-tabs{display:flex;gap:4px;margin-bottom:20px;}
2781
+ .inspector-tabs a{
2782
+ padding:7px 16px;border-radius:6px;text-decoration:none;
2783
+ font-size:.8125rem;color:#1a8c00;border:1px solid transparent;
2784
+ transition:color .15s,border-color .15s;
2785
+ }
2786
+ .inspector-tabs a:hover{color:#33ff00;}
2787
+ .inspector-tabs a.active{color:#33ff00;font-weight:600;border-color:#0a3300;background:#0a3300;}
2788
+ .inspector-section{margin-bottom:24px;}
2789
+ .inspector-section h2{
2790
+ font-family:'Geist Pixel',monospace;
2791
+ font-size:1rem;font-weight:600;color:#33ff00;margin-bottom:10px;
2792
+ }
2793
+ .inspector-section h3{
2794
+ font-family:'Geist Pixel',monospace;
2795
+ font-size:.875rem;font-weight:600;color:#1a8c00;margin:16px 0 8px;
2796
+ }
2797
+ .inspector-table{width:100%;border-collapse:collapse;margin-bottom:12px;}
2798
+ .inspector-table th,.inspector-table td{
2799
+ text-align:left;padding:8px 12px;border-bottom:1px solid #0a3300;
2800
+ font-size:.8125rem;
2801
+ }
2802
+ .inspector-table th{color:#1a8c00;font-weight:600;font-size:.75rem;text-transform:uppercase;letter-spacing:.04em;}
2803
+ .inspector-table td{color:#33ff00;}
2804
+ .inspector-table tbody tr{transition:background .1s;}
2805
+ .inspector-table tbody tr:hover{background:#0a3300;}
2806
+ .inspector-empty{color:#1a8c00;text-align:center;padding:20px 0;font-size:.8125rem;}
2807
+
2808
+ .checkout-layout{
2809
+ display:flex;min-height:calc(100vh - 42px);
2810
+ }
2811
+ .checkout-summary{
2812
+ flex:1;background:#020;padding:48px 40px 48px 10%;
2813
+ display:flex;flex-direction:column;justify-content:center;
2814
+ border-right:1px solid #0a3300;
2815
+ }
2816
+ .checkout-form-side{
2817
+ flex:1;background:#000;padding:48px 10% 48px 40px;
2818
+ display:flex;flex-direction:column;justify-content:center;
2819
+ }
2820
+ .checkout-merchant{
2821
+ display:flex;align-items:center;gap:10px;margin-bottom:6px;
2822
+ }
2823
+ .checkout-merchant-name{
2824
+ font-family:'Geist Pixel',monospace;
2825
+ font-size:.9375rem;font-weight:600;color:#33ff00;
2826
+ }
2827
+ .checkout-test-badge{
2828
+ font-size:.625rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
2829
+ background:#0a3300;color:#1a8c00;padding:2px 8px;border-radius:4px;
2830
+ }
2831
+ .checkout-total{
2832
+ font-family:'Geist Pixel',monospace;
2833
+ font-size:2rem;font-weight:700;color:#33ff00;margin:8px 0 28px;
2834
+ }
2835
+ .checkout-line-item{
2836
+ display:flex;align-items:center;gap:14px;padding:14px 0;
2837
+ border-bottom:1px solid #0a3300;
2838
+ }
2839
+ .checkout-line-item:first-child{border-top:1px solid #0a3300;}
2840
+ .checkout-item-icon{
2841
+ width:42px;height:42px;border-radius:6px;background:#0a3300;
2842
+ display:flex;align-items:center;justify-content:center;flex-shrink:0;
2843
+ font-family:'Geist Pixel',monospace;font-size:.875rem;font-weight:700;color:#116600;
2844
+ }
2845
+ .checkout-item-details{flex:1;min-width:0;}
2846
+ .checkout-item-name{font-size:.875rem;font-weight:600;color:#33ff00;}
2847
+ .checkout-item-qty{font-size:.75rem;color:#1a8c00;margin-top:2px;}
2848
+ .checkout-item-price{
2849
+ font-size:.875rem;font-weight:600;color:#33ff00;text-align:right;white-space:nowrap;
2850
+ }
2851
+ .checkout-item-unit{font-size:.6875rem;color:#1a8c00;text-align:right;margin-top:2px;}
2852
+ .checkout-totals{margin-top:20px;}
2853
+ .checkout-totals-row{
2854
+ display:flex;justify-content:space-between;padding:6px 0;
2855
+ font-size:.8125rem;color:#1a8c00;
2856
+ }
2857
+ .checkout-totals-row.total{
2858
+ border-top:1px solid #0a3300;margin-top:8px;padding-top:14px;
2859
+ font-size:.9375rem;font-weight:600;color:#33ff00;
2860
+ }
2861
+ .checkout-form-section{margin-bottom:24px;}
2862
+ .checkout-form-label{
2863
+ font-size:.8125rem;font-weight:600;color:#33ff00;margin-bottom:8px;display:block;
2864
+ }
2865
+ .checkout-input{
2866
+ width:100%;padding:10px 12px;border:1px solid #0a3300;border-radius:6px;
2867
+ background:#020;color:#33ff00;font:inherit;font-size:.875rem;
2868
+ transition:border-color .15s;outline:none;
2869
+ }
2870
+ .checkout-input:focus{border-color:#33ff00;}
2871
+ .checkout-input::placeholder{color:#116600;}
2872
+ .checkout-card-box{
2873
+ border:1px solid #0a3300;border-radius:6px;padding:14px;
2874
+ background:#020;
2875
+ }
2876
+ .checkout-card-row{
2877
+ display:flex;gap:12px;margin-top:10px;
2878
+ }
2879
+ .checkout-card-row .checkout-input{flex:1;}
2880
+ .checkout-sim-note{
2881
+ font-size:.6875rem;color:#1a8c00;margin-top:10px;text-align:center;
2882
+ font-style:italic;
2883
+ }
2884
+ .checkout-pay-btn{
2885
+ width:100%;padding:14px;border:none;border-radius:8px;
2886
+ background:#33ff00;color:#000;font:inherit;font-size:.9375rem;font-weight:700;
2887
+ cursor:pointer;transition:background .15s;
2888
+ font-family:'Geist Pixel',monospace;
2889
+ }
2890
+ .checkout-pay-btn:hover{background:#44ff22;}
2891
+ .checkout-cancel{
2892
+ text-align:center;margin-top:14px;
2893
+ }
2894
+ .checkout-cancel a{
2895
+ color:#1a8c00;text-decoration:none;font-size:.8125rem;
2896
+ transition:color .15s;
2897
+ }
2898
+ .checkout-cancel a:hover{color:#33ff00;}
2899
+ @media(max-width:768px){
2900
+ .checkout-layout{flex-direction:column;}
2901
+ .checkout-summary{padding:32px 20px;border-right:none;border-bottom:1px solid #0a3300;}
2902
+ .checkout-form-side{padding:32px 20px;}
2903
+ }
2790
2904
  `;
2791
2905
  var POWERED_BY = `<div class="powered-by">Powered by <a href="https://emulate.dev" target="_blank" rel="noopener">emulate</a></div>`;
2792
2906
  function emuBar(service) {
@@ -2806,6 +2920,7 @@ function head(title) {
2806
2920
  <head>
2807
2921
  <meta charset="utf-8"/>
2808
2922
  <meta name="viewport" content="width=device-width,initial-scale=1"/>
2923
+ <link rel="icon" href="/_emulate/favicon.ico"/>
2809
2924
  <title>${escapeHtml(title)} | emulate</title>
2810
2925
  <style>${CSS}</style>
2811
2926
  </head>`;
@@ -2909,6 +3024,7 @@ async function createIdToken(user, clientId, nonce, baseUrl) {
2909
3024
  family_name: user.family_name,
2910
3025
  picture: user.picture,
2911
3026
  locale: user.locale,
3027
+ ...user.hd ? { hd: user.hd } : {},
2912
3028
  ...nonce ? { nonce } : {}
2913
3029
  }).setProtectedHeader({ alg: "HS256", typ: "JWT" }).setIssuer(baseUrl).setAudience(clientId).setIssuedAt().setExpirationTime("1h");
2914
3030
  return builder.sign(JWT_SECRET);
@@ -2936,7 +3052,8 @@ function oauthRoutes({ app, store, baseUrl, tokenMap }) {
2936
3052
  "given_name",
2937
3053
  "family_name",
2938
3054
  "picture",
2939
- "locale"
3055
+ "locale",
3056
+ "hd"
2940
3057
  ],
2941
3058
  code_challenge_methods_supported: ["plain", "S256"]
2942
3059
  });
@@ -2964,7 +3081,11 @@ function oauthRoutes({ app, store, baseUrl, tokenMap }) {
2964
3081
  }
2965
3082
  if (redirect_uri && !matchesRedirectUri(redirect_uri, client.redirect_uris)) {
2966
3083
  return c.html(
2967
- renderErrorPage("Redirect URI mismatch", "The redirect_uri is not registered for this application.", SERVICE_LABEL),
3084
+ renderErrorPage(
3085
+ "Redirect URI mismatch",
3086
+ "The redirect_uri is not registered for this application.",
3087
+ SERVICE_LABEL
3088
+ ),
2968
3089
  400
2969
3090
  );
2970
3091
  }
@@ -3076,7 +3197,13 @@ function oauthRoutes({ app, store, baseUrl, tokenMap }) {
3076
3197
  });
3077
3198
  }
3078
3199
  if (grant_type !== "authorization_code") {
3079
- return c.json({ error: "unsupported_grant_type", error_description: "Only authorization_code and refresh_token are supported." }, 400);
3200
+ return c.json(
3201
+ {
3202
+ error: "unsupported_grant_type",
3203
+ error_description: "Only authorization_code and refresh_token are supported."
3204
+ },
3205
+ 400
3206
+ );
3080
3207
  }
3081
3208
  const pendingMap = getPendingCodes(store);
3082
3209
  const pending = pendingMap.get(code);
@@ -3149,7 +3276,8 @@ function oauthRoutes({ app, store, baseUrl, tokenMap }) {
3149
3276
  given_name: user.given_name,
3150
3277
  family_name: user.family_name,
3151
3278
  picture: user.picture,
3152
- locale: user.locale
3279
+ locale: user.locale,
3280
+ ...user.hd ? { hd: user.hd } : {}
3153
3281
  });
3154
3282
  });
3155
3283
  app.post("/oauth2/revoke", async (c) => {
@@ -3201,7 +3329,13 @@ function settingsRoutes({ app, store }) {
3201
3329
  }
3202
3330
  const missingLabelIds = findMissingLabelIds(gs, authEmail, [...addLabelIds, ...removeLabelIds]);
3203
3331
  if (missingLabelIds.length > 0) {
3204
- return googleApiError(c, 400, `Invalid label IDs: ${missingLabelIds.join(", ")}`, "invalidArgument", "INVALID_ARGUMENT");
3332
+ return googleApiError(
3333
+ c,
3334
+ 400,
3335
+ `Invalid label IDs: ${missingLabelIds.join(", ")}`,
3336
+ "invalidArgument",
3337
+ "INVALID_ARGUMENT"
3338
+ );
3205
3339
  }
3206
3340
  if (findMatchingFilter(gs, {
3207
3341
  user_email: authEmail,
@@ -3306,14 +3440,16 @@ function threadRoutes({ app, store }) {
3306
3440
  const removeLabelIds = getStringArray(body, "removeLabelIds");
3307
3441
  const missingLabelIds = findMissingLabelIds(gs, authEmail, [...addLabelIds, ...removeLabelIds]);
3308
3442
  if (missingLabelIds.length > 0) {
3309
- return googleApiError(c, 400, `Invalid label IDs: ${missingLabelIds.join(", ")}`, "invalidArgument", "INVALID_ARGUMENT");
3443
+ return googleApiError(
3444
+ c,
3445
+ 400,
3446
+ `Invalid label IDs: ${missingLabelIds.join(", ")}`,
3447
+ "invalidArgument",
3448
+ "INVALID_ARGUMENT"
3449
+ );
3310
3450
  }
3311
3451
  const updated = messages.map(
3312
- (message) => markMessageModified(
3313
- gs,
3314
- message,
3315
- applyLabelMutation(message.label_ids, addLabelIds, removeLabelIds)
3316
- )
3452
+ (message) => markMessageModified(gs, message, applyLabelMutation(message.label_ids, addLabelIds, removeLabelIds))
3317
3453
  );
3318
3454
  return c.json(formatThreadResource(gs, updated, "full"));
3319
3455
  });
@@ -3364,120 +3500,148 @@ function seedDefaults(store, _baseUrl) {
3364
3500
  family_name: "User",
3365
3501
  picture: null,
3366
3502
  email_verified: true,
3367
- locale: "en"
3503
+ locale: "en",
3504
+ hd: null
3368
3505
  });
3369
3506
  }
3370
3507
  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);
3508
+ seedCalendars(
3509
+ store,
3510
+ [
3511
+ {
3512
+ id: "primary",
3513
+ user_email: defaultEmail,
3514
+ summary: defaultEmail,
3515
+ primary: true,
3516
+ selected: true,
3517
+ time_zone: "UTC"
3518
+ },
3519
+ {
3520
+ id: "cal_team",
3521
+ user_email: defaultEmail,
3522
+ summary: "Team Calendar",
3523
+ description: "Shared team events",
3524
+ selected: true,
3525
+ time_zone: "UTC"
3526
+ }
3527
+ ],
3528
+ defaultEmail
3529
+ );
3530
+ seedCalendarEvents(
3531
+ store,
3532
+ [
3533
+ {
3534
+ id: "evt_standup",
3535
+ user_email: defaultEmail,
3536
+ calendar_id: "primary",
3537
+ summary: "Daily Standup",
3538
+ description: "Team sync",
3539
+ start_date_time: new Date(Date.now() + 60 * 60 * 1e3).toISOString(),
3540
+ end_date_time: new Date(Date.now() + 90 * 60 * 1e3).toISOString(),
3541
+ attendees: [
3542
+ { email: defaultEmail, display_name: "Test User" },
3543
+ { email: "teammate@example.com", display_name: "Teammate" }
3544
+ ],
3545
+ conference_entry_points: [
3546
+ {
3547
+ entry_point_type: "video",
3548
+ uri: "https://meet.google.com/emulate-standup",
3549
+ label: "Google Meet"
3550
+ }
3551
+ ],
3552
+ hangout_link: "https://meet.google.com/emulate-standup"
3553
+ }
3554
+ ],
3555
+ defaultEmail
3556
+ );
3557
+ seedDriveItems(
3558
+ store,
3559
+ [
3560
+ {
3561
+ id: "drv_root_receipts",
3562
+ user_email: defaultEmail,
3563
+ name: "Receipts",
3564
+ mime_type: "application/vnd.google-apps.folder",
3565
+ parent_ids: ["root"]
3566
+ },
3567
+ {
3568
+ id: "drv_receipt_pdf",
3569
+ user_email: defaultEmail,
3570
+ name: "March Receipt.pdf",
3571
+ mime_type: "application/pdf",
3572
+ parent_ids: ["drv_root_receipts"],
3573
+ data: "receipt-pdf-data"
3574
+ }
3575
+ ],
3576
+ defaultEmail
3577
+ );
3578
+ seedMessages(
3579
+ store,
3580
+ [
3581
+ {
3582
+ id: "msg_welcome",
3583
+ thread_id: "thr_welcome",
3584
+ user_email: defaultEmail,
3585
+ from: "Welcome Team <welcome@example.com>",
3586
+ to: defaultEmail,
3587
+ subject: "Welcome to your local Gmail emulator",
3588
+ snippet: "Your OAuth flow is set up and Gmail message, thread, and label APIs are ready.",
3589
+ 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.",
3590
+ label_ids: ["INBOX", "UNREAD", "CATEGORY_UPDATES"],
3591
+ date: new Date(Date.now() - 60 * 60 * 1e3).toISOString()
3592
+ },
3593
+ {
3594
+ id: "msg_build",
3595
+ thread_id: "thr_build",
3596
+ user_email: defaultEmail,
3597
+ from: "Build Bot <builds@example.com>",
3598
+ to: defaultEmail,
3599
+ subject: "Nightly build finished successfully",
3600
+ snippet: "The latest build completed successfully in 6 minutes.",
3601
+ body_text: "The latest build completed successfully in 6 minutes.\n\nArtifact upload finished and smoke checks passed.",
3602
+ label_ids: ["INBOX", "CATEGORY_UPDATES"],
3603
+ date: new Date(Date.now() - 2 * 60 * 60 * 1e3).toISOString()
3604
+ },
3605
+ {
3606
+ id: "msg_build_reply",
3607
+ thread_id: "thr_build",
3608
+ user_email: defaultEmail,
3609
+ from: defaultEmail,
3610
+ to: "Build Bot <builds@example.com>",
3611
+ subject: "Re: Nightly build finished successfully",
3612
+ snippet: "Thanks, I will review the artifact after lunch.",
3613
+ body_text: "Thanks, I will review the artifact after lunch.",
3614
+ label_ids: ["SENT"],
3615
+ date: new Date(Date.now() - 90 * 60 * 1e3).toISOString(),
3616
+ in_reply_to: "<msg_build@emulate.google.local>",
3617
+ references: "<msg_build@emulate.google.local>"
3618
+ },
3619
+ {
3620
+ id: "msg_draft",
3621
+ thread_id: "thr_draft",
3622
+ user_email: defaultEmail,
3623
+ from: defaultEmail,
3624
+ to: "someone@example.com",
3625
+ subject: "Draft follow-up",
3626
+ snippet: "Checking in on the open question from yesterday.",
3627
+ body_text: "Checking in on the open question from yesterday.",
3628
+ label_ids: ["DRAFT"],
3629
+ date: new Date(Date.now() - 30 * 60 * 1e3).toISOString()
3630
+ }
3631
+ ],
3632
+ defaultEmail
3633
+ );
3634
+ }
3635
+ var CONSUMER_EMAIL_DOMAINS = /* @__PURE__ */ new Set(["gmail.com", "googlemail.com"]);
3636
+ function deriveHd(email) {
3637
+ const domain = email.split("@")[1]?.toLowerCase();
3638
+ if (!domain) return null;
3639
+ if (CONSUMER_EMAIL_DOMAINS.has(domain)) return null;
3640
+ return domain;
3641
+ }
3642
+ function resolveHd(user) {
3643
+ if (user.hd !== void 0) return user.hd || null;
3644
+ return deriveHd(user.email);
3481
3645
  }
3482
3646
  function seedFromConfig(store, _baseUrl, config) {
3483
3647
  const gs = getGoogleStore(store);
@@ -3494,7 +3658,8 @@ function seedFromConfig(store, _baseUrl, config) {
3494
3658
  family_name: user.family_name ?? nameParts.slice(1).join(" "),
3495
3659
  picture: user.picture ?? null,
3496
3660
  email_verified: user.email_verified ?? true,
3497
- locale: user.locale ?? "en"
3661
+ locale: user.locale ?? "en",
3662
+ hd: resolveHd(user)
3498
3663
  });
3499
3664
  }
3500
3665
  ensureSystemLabels(gs, user.email);
@@ -3555,29 +3720,33 @@ function seedMessages(store, messages, fallbackEmail) {
3555
3720
  const userEmail = message.user_email ?? fallbackEmail;
3556
3721
  ensureSystemLabels(gs, userEmail);
3557
3722
  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
- });
3723
+ createStoredMessage(
3724
+ gs,
3725
+ {
3726
+ gmail_id: message.id,
3727
+ thread_id: message.thread_id,
3728
+ user_email: userEmail,
3729
+ raw: message.raw ?? null,
3730
+ from: message.from,
3731
+ to: message.to,
3732
+ cc: message.cc ?? null,
3733
+ bcc: message.bcc ?? null,
3734
+ reply_to: message.reply_to ?? null,
3735
+ subject: message.subject,
3736
+ snippet: message.snippet,
3737
+ body_text: message.body_text ?? null,
3738
+ body_html: message.body_html ?? null,
3739
+ label_ids: message.label_ids ?? ["INBOX", "UNREAD"],
3740
+ date: message.date,
3741
+ internal_date: message.internal_date,
3742
+ message_id: message.message_id,
3743
+ references: message.references ?? null,
3744
+ in_reply_to: message.in_reply_to ?? null
3745
+ },
3746
+ {
3747
+ createMissingCustomLabels: true
3748
+ }
3749
+ );
3581
3750
  }
3582
3751
  }
3583
3752
  function seedCalendars(store, calendars, fallbackEmail) {