openmates 0.12.0-alpha.27 → 0.12.0-alpha.29

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.
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-AXNRPVLE.js";
5
5
 
6
6
  // src/client.ts
7
- import { randomUUID as randomUUID2 } from "crypto";
7
+ import { randomUUID as randomUUID3 } from "crypto";
8
8
  import { platform as platform2, release } from "os";
9
9
  import { createInterface } from "readline/promises";
10
10
  import { stdin, stdout } from "process";
@@ -2048,6 +2048,163 @@ function buildEmbedShareUrl(origin, embedId, blob) {
2048
2048
  return `${origin}/share/embed/${embedId}#key=${blob}`;
2049
2049
  }
2050
2050
 
2051
+ // src/connectedAccountImport.ts
2052
+ import { createHash as createHash4, randomUUID as randomUUID2, webcrypto as webcrypto5 } from "crypto";
2053
+ var TRANSFER_PREFIX = "OMCA1.";
2054
+ var SUPPORTED_KDF_ITERATIONS = 1e5;
2055
+ async function decryptConnectedAccountCliTransferPayload(encryptedPayload, passcode) {
2056
+ if (!encryptedPayload.startsWith(TRANSFER_PREFIX)) {
2057
+ throw new Error("Connected account import payload must start with OMCA1.");
2058
+ }
2059
+ if (!passcode.trim()) {
2060
+ throw new Error("A passcode is required to import a connected account.");
2061
+ }
2062
+ const envelope = parseEnvelope(encryptedPayload.slice(TRANSFER_PREFIX.length));
2063
+ if (envelope.version !== 1 || envelope.kdf.name !== "PBKDF2-SHA256" || envelope.kdf.iterations !== SUPPORTED_KDF_ITERATIONS || envelope.cipher.name !== "AES-256-GCM") {
2064
+ throw new Error("Unsupported connected account import payload format.");
2065
+ }
2066
+ try {
2067
+ const key = await deriveTransferKey(passcode, base64UrlToBytes(envelope.kdf.salt), envelope.kdf.iterations);
2068
+ const plaintext = await webcrypto5.subtle.decrypt(
2069
+ { name: "AES-GCM", iv: toArrayBuffer3(base64UrlToBytes(envelope.cipher.iv)) },
2070
+ key,
2071
+ toArrayBuffer3(base64UrlToBytes(envelope.cipher.text))
2072
+ );
2073
+ return validateTransferPayload(JSON.parse(new TextDecoder().decode(plaintext)));
2074
+ } catch (error) {
2075
+ if (error instanceof Error && error.message.startsWith("Unsupported")) {
2076
+ throw error;
2077
+ }
2078
+ throw new Error("Could not decrypt connected account import payload. Check the passcode and payload.");
2079
+ }
2080
+ }
2081
+ async function buildEncryptedConnectedAccountImportRow(params) {
2082
+ const accountId = randomUUID2();
2083
+ const providerId = params.payload.provider_id;
2084
+ const appId = normalizeAppId(params.payload.app_id || appIdForProvider(providerId));
2085
+ const capabilities = normalizeCapabilities(params.payload.capabilities);
2086
+ const allowedActions = actionsForCapabilities(capabilities);
2087
+ const scopes = Array.isArray(params.payload.refresh_token_bundle.scopes) ? params.payload.refresh_token_bundle.scopes.filter((item) => typeof item === "string") : [];
2088
+ const label = params.payload.label.trim() || defaultProviderLabel(providerId);
2089
+ const accountRef = params.payload.account_ref?.trim() || accountId;
2090
+ return {
2091
+ id: accountId,
2092
+ hashed_user_id: sha256Hex(params.userId),
2093
+ encrypted_provider_type: await encryptJsonOrString(providerId, params.masterKey),
2094
+ provider_type_hash: sha256Hex(providerId),
2095
+ encrypted_account_label: await encryptJsonOrString(label, params.masterKey),
2096
+ encrypted_refresh_token_bundle: await encryptJsonOrString(params.payload.refresh_token_bundle, params.masterKey),
2097
+ encrypted_capabilities: await encryptJsonOrString(capabilities, params.masterKey),
2098
+ encrypted_app_permissions: await encryptJsonOrString(
2099
+ {
2100
+ app_id: appId,
2101
+ allowed_actions: allowedActions,
2102
+ scopes
2103
+ },
2104
+ params.masterKey
2105
+ ),
2106
+ encrypted_account_directory_hint: await encryptJsonOrString(
2107
+ {
2108
+ account_ref: accountRef,
2109
+ label,
2110
+ capabilities,
2111
+ runtime_modes: Object.keys(params.payload.runtime_modes).length ? params.payload.runtime_modes : runtimeModesForActions(allowedActions)
2112
+ },
2113
+ params.masterKey
2114
+ )
2115
+ };
2116
+ }
2117
+ function parseEnvelope(encodedEnvelope) {
2118
+ try {
2119
+ return JSON.parse(new TextDecoder().decode(base64UrlToBytes(encodedEnvelope)));
2120
+ } catch {
2121
+ throw new Error("Connected account import payload is malformed.");
2122
+ }
2123
+ }
2124
+ function validateTransferPayload(value) {
2125
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2126
+ throw new Error("Connected account import payload is malformed.");
2127
+ }
2128
+ const payload = value;
2129
+ if (payload.version !== 1) throw new Error("Unsupported connected account import payload format.");
2130
+ if (typeof payload.provider_id !== "string" || !payload.provider_id) throw new Error("Import payload is missing provider_id.");
2131
+ if (typeof payload.app_id !== "string" || !payload.app_id) throw new Error("Import payload is missing app_id.");
2132
+ if (typeof payload.label !== "string") throw new Error("Import payload is missing label.");
2133
+ if (!Array.isArray(payload.capabilities)) throw new Error("Import payload is missing capabilities.");
2134
+ if (!payload.runtime_modes || typeof payload.runtime_modes !== "object" || Array.isArray(payload.runtime_modes)) {
2135
+ throw new Error("Import payload is missing runtime_modes.");
2136
+ }
2137
+ if (!payload.refresh_token_bundle || typeof payload.refresh_token_bundle !== "object" || Array.isArray(payload.refresh_token_bundle) || typeof payload.refresh_token_bundle.refresh_token !== "string" || !payload.refresh_token_bundle.refresh_token) {
2138
+ throw new Error("Import payload is missing refresh token material.");
2139
+ }
2140
+ return payload;
2141
+ }
2142
+ async function deriveTransferKey(passcode, salt, iterations) {
2143
+ const keyMaterial = await webcrypto5.subtle.importKey(
2144
+ "raw",
2145
+ new TextEncoder().encode(passcode),
2146
+ "PBKDF2",
2147
+ false,
2148
+ ["deriveKey"]
2149
+ );
2150
+ return webcrypto5.subtle.deriveKey(
2151
+ {
2152
+ name: "PBKDF2",
2153
+ salt: toArrayBuffer3(salt),
2154
+ iterations,
2155
+ hash: "SHA-256"
2156
+ },
2157
+ keyMaterial,
2158
+ { name: "AES-GCM", length: 256 },
2159
+ false,
2160
+ ["decrypt"]
2161
+ );
2162
+ }
2163
+ async function encryptJsonOrString(value, masterKey) {
2164
+ const plaintext = typeof value === "string" ? value : JSON.stringify(value);
2165
+ return encryptWithAesGcmCombined(plaintext, masterKey);
2166
+ }
2167
+ function normalizeCapabilities(value) {
2168
+ if (!Array.isArray(value)) return ["read"];
2169
+ const capabilities = value.filter((item) => typeof item === "string" && item.trim().length > 0);
2170
+ return capabilities.length ? Array.from(new Set(capabilities)) : ["read"];
2171
+ }
2172
+ function actionsForCapabilities(capabilities) {
2173
+ const actions = /* @__PURE__ */ new Set();
2174
+ for (const capability of capabilities) {
2175
+ if (capability === "read") actions.add("read");
2176
+ if (capability === "write") {
2177
+ actions.add("write");
2178
+ actions.add("update");
2179
+ }
2180
+ if (capability === "delete") actions.add("delete");
2181
+ }
2182
+ return actions.size ? Array.from(actions) : ["read"];
2183
+ }
2184
+ function runtimeModesForActions(actions) {
2185
+ return Object.fromEntries(actions.map((action) => [action, action === "read" ? "allow_automatically" : "always_ask"]));
2186
+ }
2187
+ function normalizeAppId(appId) {
2188
+ return appId === "google_calendar" ? "calendar" : appId;
2189
+ }
2190
+ function appIdForProvider(providerId) {
2191
+ return providerId === "google_calendar" ? "calendar" : providerId;
2192
+ }
2193
+ function defaultProviderLabel(providerId) {
2194
+ return providerId === "google_calendar" ? "Google Calendar" : "Connected account";
2195
+ }
2196
+ function sha256Hex(value) {
2197
+ return createHash4("sha256").update(value).digest("hex");
2198
+ }
2199
+ function base64UrlToBytes(value) {
2200
+ return base64ToBytes(value.replace(/-/g, "+").replace(/_/g, "/"));
2201
+ }
2202
+ function toArrayBuffer3(input) {
2203
+ const output = new ArrayBuffer(input.byteLength);
2204
+ new Uint8Array(output).set(input);
2205
+ return output;
2206
+ }
2207
+
2051
2208
  // src/client.ts
2052
2209
  function normalizeUnixSeconds(value, fallback) {
2053
2210
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
@@ -2856,11 +3013,11 @@ var OpenMatesClient = class _OpenMatesClient {
2856
3013
  }
2857
3014
  let anonymousId = loadAnonymousId();
2858
3015
  if (!anonymousId) {
2859
- anonymousId = randomUUID2();
3016
+ anonymousId = randomUUID3();
2860
3017
  saveAnonymousId(anonymousId);
2861
3018
  }
2862
- const chatId = `anonymous-${randomUUID2()}`;
2863
- const messageId = `anonymous-message-${randomUUID2()}`;
3019
+ const chatId = `anonymous-${randomUUID3()}`;
3020
+ const messageId = `anonymous-message-${randomUUID3()}`;
2864
3021
  const requestBody = {
2865
3022
  anonymous_id: anonymousId,
2866
3023
  client_chat_id: chatId,
@@ -2918,6 +3075,59 @@ var OpenMatesClient = class _OpenMatesClient {
2918
3075
  };
2919
3076
  });
2920
3077
  }
3078
+ async importConnectedAccountFromCliPayload(params) {
3079
+ this.requireSession();
3080
+ const payload = await decryptConnectedAccountCliTransferPayload(params.encryptedPayload, params.passcode);
3081
+ const validation = await this.validateConnectedAccountImportPayload(payload);
3082
+ const user = await this.whoAmI();
3083
+ const userId = typeof user.id === "string" ? user.id : typeof user.user_id === "string" ? user.user_id : "";
3084
+ if (!userId) {
3085
+ throw new Error("Could not resolve current user id for connected account import.");
3086
+ }
3087
+ const row = await buildEncryptedConnectedAccountImportRow({
3088
+ payload,
3089
+ userId,
3090
+ masterKey: this.getMasterKeyBytes()
3091
+ });
3092
+ const stored = await this.createConnectedAccountImportRow(row);
3093
+ return {
3094
+ id: stored.id,
3095
+ providerId: payload.provider_id,
3096
+ appId: payload.app_id,
3097
+ label: payload.label,
3098
+ validation
3099
+ };
3100
+ }
3101
+ async validateConnectedAccountImportPayload(payload) {
3102
+ this.requireSession();
3103
+ const response = await this.http.post(
3104
+ "/v1/connected-accounts/validate-import",
3105
+ {
3106
+ provider_id: payload.provider_id,
3107
+ app_id: payload.app_id,
3108
+ capabilities: payload.capabilities,
3109
+ refresh_token_envelope: payload.refresh_token_bundle
3110
+ },
3111
+ this.getCliRequestHeaders()
3112
+ );
3113
+ if (!response.ok || response.data.valid !== true) {
3114
+ throw new Error(`Connected account validation failed (HTTP ${response.status})`);
3115
+ }
3116
+ assertNoConnectedAccountSecretLeak(response.data);
3117
+ return response.data;
3118
+ }
3119
+ async createConnectedAccountImportRow(row) {
3120
+ assertNoConnectedAccountSecretLeak(row);
3121
+ const response = await this.http.post(
3122
+ "/v1/connected-accounts",
3123
+ row,
3124
+ this.getCliRequestHeaders()
3125
+ );
3126
+ if (!response.ok || !response.data.id) {
3127
+ throw new Error(`Failed to store connected account import (HTTP ${response.status})`);
3128
+ }
3129
+ return response.data;
3130
+ }
2921
3131
  async cancelConnectedAccountAction(params) {
2922
3132
  const response = await this.http.post(
2923
3133
  `/v1/connected-accounts/actions/${encodeURIComponent(params.actionId)}/cancel`,
@@ -2994,7 +3204,7 @@ var OpenMatesClient = class _OpenMatesClient {
2994
3204
  pin: pin.trim().toUpperCase(),
2995
3205
  token
2996
3206
  });
2997
- const sessionId = randomUUID2();
3207
+ const sessionId = randomUUID3();
2998
3208
  const login = await this.http.post(
2999
3209
  "/v1/auth/login",
3000
3210
  {
@@ -3169,7 +3379,7 @@ var OpenMatesClient = class _OpenMatesClient {
3169
3379
  }
3170
3380
  const session = {
3171
3381
  apiUrl: this.apiUrl,
3172
- sessionId: randomUUID2(),
3382
+ sessionId: randomUUID3(),
3173
3383
  wsToken: null,
3174
3384
  cookies: this.http.getCookieMap(),
3175
3385
  masterKeyExportedB64: material.masterKeyB64,
@@ -3416,8 +3626,8 @@ var OpenMatesClient = class _OpenMatesClient {
3416
3626
  );
3417
3627
  let msgEmbedIds = [];
3418
3628
  if (clientMsgId && cache.embeds.length > 0) {
3419
- const { createHash: createHash6 } = await import("crypto");
3420
- const hashed = createHash6("sha256").update(clientMsgId).digest("hex");
3629
+ const { createHash: createHash7 } = await import("crypto");
3630
+ const hashed = createHash7("sha256").update(clientMsgId).digest("hex");
3421
3631
  msgEmbedIds = cache.embeds.filter(
3422
3632
  (e) => e.hashed_message_id === hashed && // Only include parent embeds (no parent_embed_id).
3423
3633
  // Child embeds inherit the parent's key and are loaded
@@ -3464,8 +3674,8 @@ var OpenMatesClient = class _OpenMatesClient {
3464
3674
  );
3465
3675
  }
3466
3676
  const embedId = String(embed.embed_id ?? embed.id ?? "");
3467
- const { createHash: createHash6 } = await import("crypto");
3468
- const hashedEmbedId = createHash6("sha256").update(embedId).digest("hex");
3677
+ const { createHash: createHash7 } = await import("crypto");
3678
+ const hashedEmbedId = createHash7("sha256").update(embedId).digest("hex");
3469
3679
  const embedKeyBytes = await this.resolveEmbedKey(
3470
3680
  cache,
3471
3681
  masterKey,
@@ -3573,7 +3783,7 @@ var OpenMatesClient = class _OpenMatesClient {
3573
3783
  async resolveEmbedKey(cache, masterKey, embed, embedId, hashedEmbedId, visited = /* @__PURE__ */ new Set()) {
3574
3784
  if (visited.has(embedId)) return null;
3575
3785
  visited.add(embedId);
3576
- const { createHash: createHash6 } = await import("crypto");
3786
+ const { createHash: createHash7 } = await import("crypto");
3577
3787
  const masterKeyEntry = cache.embedKeys.find(
3578
3788
  (ek) => ek.hashed_embed_id === hashedEmbedId && String(ek.key_type) === "master"
3579
3789
  );
@@ -3590,7 +3800,7 @@ var OpenMatesClient = class _OpenMatesClient {
3590
3800
  if (chatKeyEntry && typeof chatKeyEntry.encrypted_embed_key === "string") {
3591
3801
  const hashedChatId = String(chatKeyEntry.hashed_chat_id ?? "");
3592
3802
  const owningChat = cache.chats.find((c) => {
3593
- const chatHash = createHash6("sha256").update(String(c.details.id ?? "")).digest("hex");
3803
+ const chatHash = createHash7("sha256").update(String(c.details.id ?? "")).digest("hex");
3594
3804
  return chatHash === hashedChatId;
3595
3805
  });
3596
3806
  if (owningChat) {
@@ -3619,7 +3829,7 @@ var OpenMatesClient = class _OpenMatesClient {
3619
3829
  const parentFullId = String(
3620
3830
  parentEmbed2.embed_id ?? parentEmbed2.id ?? ""
3621
3831
  );
3622
- const parentHashedId = createHash6("sha256").update(parentFullId).digest("hex");
3832
+ const parentHashedId = createHash7("sha256").update(parentFullId).digest("hex");
3623
3833
  const parentKey = await this.resolveEmbedKey(
3624
3834
  cache,
3625
3835
  masterKey,
@@ -3734,7 +3944,7 @@ var OpenMatesClient = class _OpenMatesClient {
3734
3944
  async sendMessage(params) {
3735
3945
  let chatId;
3736
3946
  if (!params.chatId) {
3737
- chatId = randomUUID2();
3947
+ chatId = randomUUID3();
3738
3948
  } else if (params.chatId.length < 36) {
3739
3949
  const resolved = await this.resolveFullChatId(params.chatId);
3740
3950
  if (!resolved) {
@@ -3764,7 +3974,7 @@ var OpenMatesClient = class _OpenMatesClient {
3764
3974
  }
3765
3975
  }
3766
3976
  const { ws, session } = await this.openWsClient();
3767
- const messageId = randomUUID2();
3977
+ const messageId = randomUUID3();
3768
3978
  const createdAt = Math.floor(Date.now() / 1e3);
3769
3979
  const isNewChat = !params.chatId;
3770
3980
  ws.send("set_active_chat", { chat_id: chatId });
@@ -4151,7 +4361,7 @@ var OpenMatesClient = class _OpenMatesClient {
4151
4361
  );
4152
4362
  const encryptedCategory = category ? await encryptWithAesGcmCombined(category, chatKeyBytes) : void 0;
4153
4363
  const encryptedModelName = modelName ? await encryptWithAesGcmCombined(modelName, chatKeyBytes) : void 0;
4154
- const persistedAssistantMessageId = assistantMessageId ?? randomUUID2();
4364
+ const persistedAssistantMessageId = assistantMessageId ?? randomUUID3();
4155
4365
  ws.send("ai_response_completed", {
4156
4366
  chat_id: chatId,
4157
4367
  message: {
@@ -5294,7 +5504,7 @@ Required: ${schema.required.join(", ")}`
5294
5504
  );
5295
5505
  }
5296
5506
  }
5297
- const entryId = params.entryId ?? randomUUID2();
5507
+ const entryId = params.entryId ?? randomUUID3();
5298
5508
  const now = Math.floor(Date.now() / 1e3);
5299
5509
  const hashedKey = hashItemKey(params.appId, params.itemType);
5300
5510
  const plaintextPayload = {
@@ -5428,7 +5638,7 @@ Required: ${schema.required.join(", ")}`
5428
5638
  const session = this.requireSession();
5429
5639
  const masterKey = base64ToBytes(session.masterKeyExportedB64);
5430
5640
  const cache = await this.ensureSynced();
5431
- const { createHash: createHash6 } = await import("crypto");
5641
+ const { createHash: createHash7 } = await import("crypto");
5432
5642
  const embed = cache.embeds.find(
5433
5643
  (e) => String(e.embed_id ?? "").startsWith(embedIdOrShort) || String(e.id ?? "").startsWith(embedIdOrShort)
5434
5644
  );
@@ -5436,7 +5646,7 @@ Required: ${schema.required.join(", ")}`
5436
5646
  throw new Error(`Embed '${embedIdOrShort}' not found in local cache.`);
5437
5647
  }
5438
5648
  const embedId = String(embed.embed_id ?? embed.id ?? "");
5439
- const hashedEmbedId = createHash6("sha256").update(embedId).digest("hex");
5649
+ const hashedEmbedId = createHash7("sha256").update(embedId).digest("hex");
5440
5650
  const embedKeyBytes = await this.resolveEmbedKey(
5441
5651
  cache,
5442
5652
  masterKey,
@@ -5496,8 +5706,8 @@ Required: ${schema.required.join(", ")}`
5496
5706
  if (!embed) {
5497
5707
  throw new Error(`Embed '${embedId}' not found in local cache. Run 'openmates chats list' to sync first.`);
5498
5708
  }
5499
- const { createHash: createHash6 } = await import("crypto");
5500
- const hashedEmbedId = createHash6("sha256").update(embedId).digest("hex");
5709
+ const { createHash: createHash7 } = await import("crypto");
5710
+ const hashedEmbedId = createHash7("sha256").update(embedId).digest("hex");
5501
5711
  const embedKey = await this.resolveEmbedKey(
5502
5712
  cache,
5503
5713
  masterKey,
@@ -5591,8 +5801,8 @@ Required: ${schema.required.join(", ")}`
5591
5801
  if (!embed) {
5592
5802
  throw new Error(`Embed '${embedId}' not found in local cache. Run 'openmates chats list' to sync first.`);
5593
5803
  }
5594
- const { createHash: createHash6 } = await import("crypto");
5595
- const hashedEmbedId = createHash6("sha256").update(embedId).digest("hex");
5804
+ const { createHash: createHash7 } = await import("crypto");
5805
+ const hashedEmbedId = createHash7("sha256").update(embedId).digest("hex");
5596
5806
  const embedKey = await this.resolveEmbedKey(
5597
5807
  cache,
5598
5808
  masterKey,
@@ -5769,7 +5979,7 @@ Required: ${schema.required.join(", ")}`
5769
5979
  makeWsClient(session) {
5770
5980
  return new OpenMatesWsClient({
5771
5981
  apiUrl: session.apiUrl,
5772
- sessionId: randomUUID2(),
5982
+ sessionId: randomUUID3(),
5773
5983
  wsToken: session.wsToken,
5774
5984
  refreshToken: session.cookies.auth_refresh_token ?? null,
5775
5985
  // Same User-Agent as login so OS-based device fingerprint hash matches.
@@ -6929,7 +7139,7 @@ var OutputRedactor = class {
6929
7139
  import { readFileSync as readFileSync4, statSync, existsSync as existsSync4 } from "fs";
6930
7140
  import { basename, extname, resolve as resolve2 } from "path";
6931
7141
  import { homedir as homedir4 } from "os";
6932
- import { createHash as createHash4 } from "crypto";
7142
+ import { createHash as createHash5 } from "crypto";
6933
7143
  var MAX_PER_FILE_SIZE = 100 * 1024 * 1024;
6934
7144
  var BLOCKED_EXTENSIONS = /* @__PURE__ */ new Set([
6935
7145
  ".pem",
@@ -7239,7 +7449,7 @@ function processCodeFile(filePath, filename, redactor) {
7239
7449
  line_count: lineCount
7240
7450
  });
7241
7451
  const textPreview = `${filename} (${language}, ${lineCount} lines)`;
7242
- const contentHash = createHash4("sha256").update(content).digest("hex");
7452
+ const contentHash = createHash5("sha256").update(content).digest("hex");
7243
7453
  const embed = {
7244
7454
  embedId,
7245
7455
  embedRef,
@@ -8754,8 +8964,8 @@ function formatTs(ts) {
8754
8964
  }
8755
8965
 
8756
8966
  // src/server.ts
8757
- import { execSync, spawn as nodeSpawn } from "child_process";
8758
- import { createHash as createHash5, randomBytes as randomBytes2 } from "crypto";
8967
+ import { execFileSync as execFileSync2, execSync, spawn as nodeSpawn } from "child_process";
8968
+ import { createHash as createHash6, randomBytes as randomBytes2 } from "crypto";
8759
8969
  import { chmodSync as chmodSync2, copyFileSync, cpSync, existsSync as existsSync5, mkdirSync as mkdirSync3, mkdtempSync, readFileSync as readFileSync5, readdirSync, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
8760
8970
  import { createInterface as createInterface2 } from "readline";
8761
8971
  import { createInterface as createPromptInterface } from "readline/promises";
@@ -8894,6 +9104,49 @@ function planRestore(input) {
8894
9104
  steps: input.yes === true ? ["stop", "restore", "start", "health-check"] : ["confirm", "stop", "restore", "start", "health-check"]
8895
9105
  };
8896
9106
  }
9107
+ function parseSecretEnvKey(envKey) {
9108
+ if (!envKey.startsWith("SECRET__")) return null;
9109
+ const parts = envKey.slice("SECRET__".length).split("__", 2);
9110
+ if (parts.length !== 2 || !parts[0] || !parts[1]) return null;
9111
+ return {
9112
+ envKey,
9113
+ vaultPath: `kv/data/providers/${parts[0].toLowerCase()}`,
9114
+ vaultKey: parts[1].toLowerCase()
9115
+ };
9116
+ }
9117
+ function summarizeSecretPreflight(input) {
9118
+ const inlineSecretEnvKeys = [];
9119
+ const importedSecretEnvKeys = [];
9120
+ const emptySecretEnvKeys = [];
9121
+ const importedVaultPresent = [];
9122
+ const importedVaultMissing = [];
9123
+ const importedVaultUnavailable = [];
9124
+ for (const [envKey, rawValue] of Object.entries(input.env).sort(([a], [b]) => a.localeCompare(b))) {
9125
+ if (!parseSecretEnvKey(envKey)) continue;
9126
+ const value = rawValue.trim();
9127
+ if (!value) {
9128
+ emptySecretEnvKeys.push(envKey);
9129
+ continue;
9130
+ }
9131
+ if (value !== "IMPORTED_TO_VAULT") {
9132
+ inlineSecretEnvKeys.push(envKey);
9133
+ continue;
9134
+ }
9135
+ importedSecretEnvKeys.push(envKey);
9136
+ const presence = input.vaultPresence?.[envKey];
9137
+ if (presence === "present") importedVaultPresent.push(envKey);
9138
+ else if (presence === "missing") importedVaultMissing.push(envKey);
9139
+ else importedVaultUnavailable.push(envKey);
9140
+ }
9141
+ return {
9142
+ inlineSecretEnvKeys,
9143
+ importedSecretEnvKeys,
9144
+ emptySecretEnvKeys,
9145
+ importedVaultPresent,
9146
+ importedVaultMissing,
9147
+ importedVaultUnavailable
9148
+ };
9149
+ }
8897
9150
  function planCaddyCommand(input) {
8898
9151
  const role = parseServerRole(input.role);
8899
9152
  const templatePath = `templates/caddy/${role}/Caddyfile`;
@@ -9321,7 +9574,7 @@ function packagedCaddyTemplatePath(role) {
9321
9574
  }
9322
9575
  function fileHash(path) {
9323
9576
  if (!existsSync5(path)) return null;
9324
- return createHash5("sha256").update(readFileSync5(path)).digest("hex");
9577
+ return createHash6("sha256").update(readFileSync5(path)).digest("hex");
9325
9578
  }
9326
9579
  async function loadSelfHostComposeTemplate(templateRef, role) {
9327
9580
  const templateDir = process.env.OPENMATES_SELFHOST_TEMPLATE_DIR;
@@ -9586,6 +9839,89 @@ function missingRequiredEnvKeys(installPath, role) {
9586
9839
  const env = readEnvMap(installPath);
9587
9840
  return requiredRuntimeEnvKeys(role).filter((key) => !env[key]);
9588
9841
  }
9842
+ function vaultCheckContainerForRole(role) {
9843
+ if (role === "core") return "api";
9844
+ if (role === "upload") return "app-uploads";
9845
+ return null;
9846
+ }
9847
+ function readImportedVaultSecretPresence(role, importedSecrets) {
9848
+ const checks = importedSecrets.filter((item) => item !== null);
9849
+ if (checks.length === 0) return { presence: {}, checked: true, unavailableReason: null };
9850
+ const container = vaultCheckContainerForRole(role);
9851
+ if (!container) {
9852
+ return { presence: {}, checked: false, unavailableReason: `${role} role does not expose a Vault-backed app container for secret verification.` };
9853
+ }
9854
+ const script = `
9855
+ import json
9856
+ import os
9857
+ import urllib.error
9858
+ import urllib.request
9859
+
9860
+ checks = json.loads(os.environ["OPENMATES_SECRET_CHECKS"])
9861
+ token_path = "/vault-data/api.token"
9862
+ try:
9863
+ with open(token_path, "r", encoding="utf-8") as token_file:
9864
+ token = token_file.read().strip()
9865
+ except Exception as exc:
9866
+ print(json.dumps({"ok": False, "reason": f"could not read {token_path}: {exc}", "presence": {}}))
9867
+ raise SystemExit(0)
9868
+
9869
+ presence = {}
9870
+ for item in checks:
9871
+ request = urllib.request.Request(
9872
+ f"http://vault:8200/v1/{item['vaultPath']}",
9873
+ headers={"X-Vault-Token": token},
9874
+ )
9875
+ try:
9876
+ with urllib.request.urlopen(request, timeout=3) as response:
9877
+ payload = json.loads(response.read().decode("utf-8"))
9878
+ value = payload.get("data", {}).get("data", {}).get(item["vaultKey"])
9879
+ presence[item["envKey"]] = "present" if value else "missing"
9880
+ except urllib.error.HTTPError as exc:
9881
+ presence[item["envKey"]] = "missing" if exc.code == 404 else "unavailable"
9882
+ except Exception:
9883
+ presence[item["envKey"]] = "unavailable"
9884
+
9885
+ print(json.dumps({"ok": True, "presence": presence}))
9886
+ `;
9887
+ try {
9888
+ const output = execFileSync2(
9889
+ "docker",
9890
+ ["exec", "-e", `OPENMATES_SECRET_CHECKS=${JSON.stringify(checks)}`, container, "python", "-c", script],
9891
+ { encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
9892
+ ).trim();
9893
+ const parsed = JSON.parse(output);
9894
+ if (parsed.ok === false) return { presence: {}, checked: false, unavailableReason: parsed.reason ?? "Vault check failed." };
9895
+ return { presence: parsed.presence ?? {}, checked: true, unavailableReason: null };
9896
+ } catch (error) {
9897
+ return {
9898
+ presence: {},
9899
+ checked: false,
9900
+ unavailableReason: `Vault check unavailable. Is the ${container} container running? ${error instanceof Error ? error.message : String(error)}`
9901
+ };
9902
+ }
9903
+ }
9904
+ function runtimeSecretPreflight(installPath, role) {
9905
+ const env = readEnvMap(installPath);
9906
+ const importedSecrets = Object.entries(env).filter(([, value]) => value.trim() === "IMPORTED_TO_VAULT").map(([envKey]) => parseSecretEnvKey(envKey)).filter((item) => item !== null);
9907
+ const vault = readImportedVaultSecretPresence(role, importedSecrets);
9908
+ return {
9909
+ ...summarizeSecretPreflight({ env, vaultPresence: vault.presence }),
9910
+ vaultChecked: vault.checked,
9911
+ vaultUnavailableReason: vault.unavailableReason
9912
+ };
9913
+ }
9914
+ function formatSecretPreflight(preflight) {
9915
+ const parts = [
9916
+ `${preflight.importedVaultPresent.length}/${preflight.importedSecretEnvKeys.length} imported verified`
9917
+ ];
9918
+ if (preflight.importedVaultMissing.length) parts.push(`missing in Vault: ${preflight.importedVaultMissing.join(", ")}`);
9919
+ if (preflight.importedVaultUnavailable.length) parts.push(`unverified: ${preflight.importedVaultUnavailable.join(", ")}`);
9920
+ if (preflight.inlineSecretEnvKeys.length) parts.push(`inline SECRET entries: ${preflight.inlineSecretEnvKeys.length}`);
9921
+ if (preflight.emptySecretEnvKeys.length) parts.push(`empty SECRET entries: ${preflight.emptySecretEnvKeys.length}`);
9922
+ if (preflight.vaultUnavailableReason) parts.push(preflight.vaultUnavailableReason);
9923
+ return parts.join("; ");
9924
+ }
9589
9925
  function writeChecksums(rootDir) {
9590
9926
  const lines = [];
9591
9927
  const walk = (dir) => {
@@ -9597,7 +9933,7 @@ function writeChecksums(rootDir) {
9597
9933
  }
9598
9934
  if (entry.name === "checksums.sha256") continue;
9599
9935
  const relative2 = path.slice(rootDir.length + 1);
9600
- const hash = createHash5("sha256").update(readFileSync5(path)).digest("hex");
9936
+ const hash = createHash6("sha256").update(readFileSync5(path)).digest("hex");
9601
9937
  lines.push(`${hash} ${relative2}`);
9602
9938
  }
9603
9939
  };
@@ -9619,7 +9955,7 @@ function verifyChecksums(rootDir) {
9619
9955
  }
9620
9956
  const filePath = join3(rootDir, relative2);
9621
9957
  if (!existsSync5(filePath)) throw new Error(`Backup archive is missing checksummed file: ${relative2}`);
9622
- const actual = createHash5("sha256").update(readFileSync5(filePath)).digest("hex");
9958
+ const actual = createHash6("sha256").update(readFileSync5(filePath)).digest("hex");
9623
9959
  if (actual !== match[1]) throw new Error(`Backup checksum mismatch for ${relative2}.`);
9624
9960
  }
9625
9961
  }
@@ -10129,6 +10465,12 @@ async function serverUpdate(rest, flags) {
10129
10465
  const filterRequested = hasServiceFilter(flags);
10130
10466
  const selectedServices = filterRequested ? selectedComposeServices(role, flags) : [];
10131
10467
  const missingEnvKeys = missingRequiredEnvKeys(installPath, role);
10468
+ const secretPreflight = runtimeSecretPreflight(installPath, role);
10469
+ const missingOrUnverifiedSecrets = [
10470
+ ...missingEnvKeys,
10471
+ ...secretPreflight.importedVaultMissing,
10472
+ ...secretPreflight.importedVaultUnavailable
10473
+ ];
10132
10474
  if (installMode === "source" && (flags["image-tag"] !== void 0 || flags.channel !== void 0)) {
10133
10475
  throw new Error("--image-tag and --channel only apply to image-mode installs. Source-mode installs update from Git.");
10134
10476
  }
@@ -10138,7 +10480,7 @@ async function serverUpdate(rest, flags) {
10138
10480
  const currentTag = getImageTagFromEnv(installPath, config);
10139
10481
  const target = resolveTargetImageTag(flags, currentTag, getPackageVersion());
10140
10482
  const templateRef = templateRefForImageTag(target.tag, getPackageVersion());
10141
- const safetyPlan = planUpdate({ role, selectedServices, dryRun, skipBackup: flags["skip-backup"] === true, continuous: false, missingRequiredSecrets: missingEnvKeys });
10483
+ const safetyPlan = planUpdate({ role, selectedServices, dryRun, skipBackup: flags["skip-backup"] === true, continuous: false, missingRequiredSecrets: missingOrUnverifiedSecrets });
10142
10484
  const plan = {
10143
10485
  command: "update",
10144
10486
  role,
@@ -10152,6 +10494,7 @@ async function serverUpdate(rest, flags) {
10152
10494
  steps: safetyPlan.steps,
10153
10495
  backupName: safetyPlan.backupName,
10154
10496
  missingRequiredEnvKeys: missingEnvKeys,
10497
+ secretPreflight,
10155
10498
  blocked: safetyPlan.blocked,
10156
10499
  blockReason: safetyPlan.blockReason,
10157
10500
  dryRun
@@ -10170,13 +10513,16 @@ async function serverUpdate(rest, flags) {
10170
10513
  console.log(` Backup: ${safetyPlan.backupName ?? "none"}`);
10171
10514
  console.log(` Steps: ${safetyPlan.steps.join(" -> ")}`);
10172
10515
  console.log(` Env preflight: ${missingEnvKeys.length ? `missing ${missingEnvKeys.join(", ")}` : "ok"}`);
10516
+ console.log(` Vault secrets: ${formatSecretPreflight(secretPreflight)}`);
10173
10517
  console.log(" Commands: refresh compose, docker compose pull, docker compose up -d, health checks");
10174
10518
  }
10175
10519
  return;
10176
10520
  }
10177
10521
  if (safetyPlan.blocked) throw new Error(safetyPlan.blockReason ?? "Update blocked by preflight.");
10178
- if (missingEnvKeys.length && flags.yes !== true) {
10179
- throw new Error(`Required environment keys are missing: ${missingEnvKeys.join(", ")}. Add them to .env or rerun with --yes after reviewing.`);
10522
+ if (missingOrUnverifiedSecrets.length && flags.yes !== true) {
10523
+ throw new Error(
10524
+ `Required runtime secret checks failed: ${missingOrUnverifiedSecrets.join(", ")}. Add missing non-SECRET values to .env, ensure IMPORTED_TO_VAULT markers exist in Vault, or rerun with --yes after reviewing.`
10525
+ );
10180
10526
  }
10181
10527
  console.error(`Mode: image`);
10182
10528
  console.error(`Current image tag: ${currentTag || "unknown"}`);
@@ -10729,8 +11075,14 @@ async function serverPreflight(flags) {
10729
11075
  const config = loadConfigForInstallPath(installPath);
10730
11076
  const role = getServerRole(flags, config);
10731
11077
  const services = hasServiceFilter(flags) ? selectedComposeServices(role, flags) : [];
10732
- const updatePlan = planUpdate({ role, selectedServices: services, dryRun: true });
10733
11078
  const missingEnvKeys = missingRequiredEnvKeys(installPath, role);
11079
+ const secretPreflight = runtimeSecretPreflight(installPath, role);
11080
+ const missingOrUnverifiedSecrets = [
11081
+ ...missingEnvKeys,
11082
+ ...secretPreflight.importedVaultMissing,
11083
+ ...secretPreflight.importedVaultUnavailable
11084
+ ];
11085
+ const updatePlan = planUpdate({ role, selectedServices: services, dryRun: true, continuous: true, missingRequiredSecrets: missingOrUnverifiedSecrets });
10734
11086
  const runtimePlan = planServerRuntime({
10735
11087
  role,
10736
11088
  profile: getCoreProfile(flags, config),
@@ -10748,6 +11100,10 @@ async function serverPreflight(flags) {
10748
11100
  updateSteps: updatePlan.steps,
10749
11101
  backupName: updatePlan.backupName,
10750
11102
  missingRequiredEnvKeys: missingEnvKeys,
11103
+ secretPreflight,
11104
+ missingOrUnverifiedSecrets,
11105
+ blocked: updatePlan.blocked,
11106
+ blockReason: updatePlan.blockReason,
10751
11107
  caddy: caddyPlan
10752
11108
  };
10753
11109
  if (flags.json === true) {
@@ -10760,6 +11116,8 @@ async function serverPreflight(flags) {
10760
11116
  console.log(` Services: ${hasServiceFilter(flags) ? services.join(", ") : "all"}`);
10761
11117
  console.log(` Backup: ${updatePlan.backupName ?? "none"}`);
10762
11118
  console.log(` Env preflight: ${missingEnvKeys.length ? `missing ${missingEnvKeys.join(", ")}` : "ok"}`);
11119
+ console.log(` Vault secrets: ${formatSecretPreflight(secretPreflight)}`);
11120
+ if (updatePlan.blocked) console.log(` Blocked: ${updatePlan.blockReason}`);
10763
11121
  console.log(` Health checks: ${runtimePlan.healthChecks.join(", ")}`);
10764
11122
  console.log(` Caddy steps: ${caddyPlan.steps.join(" -> ")}`);
10765
11123
  }
@@ -10812,14 +11170,16 @@ WARNING: This will replace ${appliedPath} with the packaged ${role} Caddyfile.`)
10812
11170
  }
10813
11171
  }
10814
11172
  const backupPath = `${appliedPath}.openmates-backup-${nowStamp()}`;
11173
+ const hadExistingCaddyfile = existsSync5(appliedPath);
10815
11174
  try {
10816
- if (existsSync5(appliedPath)) copyFileSync(appliedPath, backupPath);
11175
+ mkdirSync3(dirname(appliedPath), { recursive: true });
11176
+ if (hadExistingCaddyfile) copyFileSync(appliedPath, backupPath);
10817
11177
  copyFileSync(templatePath, appliedPath);
10818
11178
  execSync("systemctl reload caddy", { stdio: "inherit" });
10819
11179
  } catch (error) {
10820
11180
  throw new Error(`Could not apply Caddyfile. Run with sudo or use --config <writable path>. ${error instanceof Error ? error.message : String(error)}`);
10821
11181
  }
10822
- console.log(`Applied Caddyfile for ${role}. Backup: ${backupPath}`);
11182
+ console.log(`Applied Caddyfile for ${role}. ${hadExistingCaddyfile ? `Backup: ${backupPath}` : "No previous Caddyfile existed."}`);
10823
11183
  return;
10824
11184
  }
10825
11185
  console.log(`Caddy ${action} plan:`);
@@ -24990,6 +25350,118 @@ var berlinMorningBikeForecastChat = {
24990
25350
  }
24991
25351
  };
24992
25352
 
25353
+ // ../ui/src/demo_chats/data/example_chats/privacy-first-product-launch-mind-map.ts
25354
+ var privacyFirstProductLaunchMindMapChat = {
25355
+ chat_id: "example-privacy-first-product-launch",
25356
+ slug: "privacy-first-product-launch-mind-map",
25357
+ title: "example_chats.privacy_first_product_launch_mind_map.title",
25358
+ summary: "example_chats.privacy_first_product_launch_mind_map.summary",
25359
+ icon: "workflow",
25360
+ category: "design",
25361
+ keywords: ["mind map", "privacy-first product launch", "product planning", "privacy UX", "launch strategy"],
25362
+ follow_up_suggestions: ["example_chats.privacy_first_product_launch_mind_map.follow_up_1", "example_chats.privacy_first_product_launch_mind_map.follow_up_2", "example_chats.privacy_first_product_launch_mind_map.follow_up_3", "example_chats.privacy_first_product_launch_mind_map.follow_up_4", "example_chats.privacy_first_product_launch_mind_map.follow_up_5", "example_chats.privacy_first_product_launch_mind_map.follow_up_6"],
25363
+ messages: [
25364
+ {
25365
+ "id": "15e39975-bc0e-4184-bf14-473d87131a6a",
25366
+ "role": "user",
25367
+ "content": "example_chats.privacy_first_product_launch_mind_map.message_1",
25368
+ "created_at": 1782037289
25369
+ },
25370
+ {
25371
+ "id": "d07c4f4f-76b2-4e71-a863-9f8baf0a359e",
25372
+ "role": "assistant",
25373
+ "content": "example_chats.privacy_first_product_launch_mind_map.message_2",
25374
+ "created_at": 1782037316,
25375
+ "user_message_id": "15e39975-bc0e-4184-bf14-473d87131a6a",
25376
+ "category": "design",
25377
+ "model_name": "Gemini 3.1 Pro"
25378
+ }
25379
+ ],
25380
+ embeds: [
25381
+ {
25382
+ "embed_id": "f9540460-1fce-441a-9abc-f627efaf1c0f",
25383
+ "type": "mindmap",
25384
+ "content": `type: mindmap
25385
+ app_id: mindmaps
25386
+ skill_id: mindmap
25387
+ title: Privacy-First Product Launch Planning
25388
+ source_json: "{\\n \\"openmatesType\\": \\"mindmap\\",\\n \\"schemaVersion\\": 1,\\n \\"title\\": \\"Privacy-First Product Launch Planning\\",\\n \\"rootId\\": \\"root\\",\\n \\"nodes\\": [\\n {\\n \\"id\\": \\"root\\",\\n \\"label\\": \\"Privacy-First Product Launch\\",\\n \\"children\\": [\\n \\"prod-eng\\",\\n \\"legal\\",\\n \\"ux\\",\\n \\"marketing\\",\\n \\"logistics\\"\\n ]\\n },\\n {\\n \\"id\\": \\"prod-eng\\",\\n \\"label\\": \\"Product & Engineering\\",\\n \\"children\\": [\\n \\"pe-data\\",\\n \\"pe-e2e\\",\\n \\"pe-audit\\"\\n ]\\n },\\n {\\n \\"id\\": \\"pe-data\\",\\n \\"label\\": \\"Data Minimization Specs\\"\\n },\\n {\\n \\"id\\": \\"pe-e2e\\",\\n \\"label\\": \\"End-to-End Encryption\\"\\n },\\n {\\n \\"id\\": \\"pe-audit\\",\\n \\"label\\": \\"3rd-Party Security Audits\\"\\n },\\n {\\n \\"id\\": \\"legal\\",\\n \\"label\\": \\"Legal & Compliance\\",\\n \\"children\\": [\\n \\"leg-gdpr\\",\\n \\"leg-policy\\",\\n \\"leg-tos\\"\\n ]\\n },\\n {\\n \\"id\\": \\"leg-gdpr\\",\\n \\"label\\": \\"GDPR / CCPA Readiness\\"\\n },\\n {\\n \\"id\\": \\"leg-policy\\",\\n \\"label\\": \\"Plain-English Privacy Policy\\"\\n },\\n {\\n \\"id\\": \\"leg-tos\\",\\n \\"label\\": \\"Transparent Terms of Service\\"\\n },\\n {\\n \\"id\\": \\"ux\\",\\n \\"label\\": \\"User Experience (UX)\\",\\n \\"children\\": [\\n \\"ux-consent\\",\\n \\"ux-dash\\",\\n \\"ux-delete\\"\\n ]\\n },\\n {\\n \\"id\\": \\"ux-consent\\",\\n \\"label\\": \\"Clear Consent Flows (No Dark Patterns)\\"\\n },\\n {\\n \\"id\\": \\"ux-dash\\",\\n \\"label\\": \\"Centralized Privacy Dashboard\\"\\n },\\n {\\n \\"id\\": \\"ux-delete\\",\\n \\"label\\": \\"1-Click Account Deletion\\"\\n },\\n {\\n \\"id\\": \\"marketing\\",\\n \\"label\\": \\"Marketing & Messaging\\",\\n \\"children\\": [\\n \\"mkt-core\\",\\n \\"mkt-press\\",\\n \\"mkt-advocates\\"\\n ]\\n },\\n {\\n \\"id\\": \\"mkt-core\\",\\n \\"label\\": \\"Core Message: 'Your Data is Yours'\\"\\n },\\n {\\n \\"id\\": \\"mkt-press\\",\\n \\"label\\": \\"Pitching Privacy Tech Journalists\\"\\n },\\n {\\n \\"id\\": \\"mkt-advocates\\",\\n \\"label\\": \\"Partnering w/ Privacy Advocates\\"\\n },\\n {\\n \\"id\\": \\"logistics\\",\\n \\"label\\": \\"Launch Logistics\\",\\n \\"children\\": [\\n \\"log-beta\\",\\n \\"log-rollout\\"\\n ]\\n },\\n {\\n \\"id\\": \\"log-beta\\",\\n \\"label\\": \\"Closed Beta & Bug Bounty\\"\\n },\\n {\\n \\"id\\": \\"log-rollout\\",\\n \\"label\\": \\"Phased Public Rollout\\"\\n }\\n ],\\n \\"edges\\": [],\\n \\"view\\": {\\n \\"layout\\": \\"radial-tree\\",\\n \\"collapsedNodeIds\\": []\\n }\\n}"
25389
+ model:
25390
+ openmatesType: mindmap
25391
+ schemaVersion: 1
25392
+ title: Privacy-First Product Launch Planning
25393
+ rootId: root
25394
+ nodes[20]:
25395
+ - id: root
25396
+ label: Privacy-First Product Launch
25397
+ children[5]: prod-eng,legal,ux,marketing,logistics
25398
+ - id: prod-eng
25399
+ label: Product & Engineering
25400
+ children[3]: pe-data,pe-e2e,pe-audit
25401
+ - id: pe-data
25402
+ label: Data Minimization Specs
25403
+ - id: pe-e2e
25404
+ label: End-to-End Encryption
25405
+ - id: pe-audit
25406
+ label: 3rd-Party Security Audits
25407
+ - id: legal
25408
+ label: Legal & Compliance
25409
+ children[3]: leg-gdpr,leg-policy,leg-tos
25410
+ - id: leg-gdpr
25411
+ label: GDPR / CCPA Readiness
25412
+ - id: leg-policy
25413
+ label: Plain-English Privacy Policy
25414
+ - id: leg-tos
25415
+ label: Transparent Terms of Service
25416
+ - id: ux
25417
+ label: User Experience (UX)
25418
+ children[3]: ux-consent,ux-dash,ux-delete
25419
+ - id: ux-consent
25420
+ label: Clear Consent Flows (No Dark Patterns)
25421
+ - id: ux-dash
25422
+ label: Centralized Privacy Dashboard
25423
+ - id: ux-delete
25424
+ label: 1-Click Account Deletion
25425
+ - id: marketing
25426
+ label: Marketing & Messaging
25427
+ children[3]: mkt-core,mkt-press,mkt-advocates
25428
+ - id: mkt-core
25429
+ label: "Core Message: 'Your Data is Yours'"
25430
+ - id: mkt-press
25431
+ label: Pitching Privacy Tech Journalists
25432
+ - id: mkt-advocates
25433
+ label: Partnering w/ Privacy Advocates
25434
+ - id: logistics
25435
+ label: Launch Logistics
25436
+ children[2]: log-beta,log-rollout
25437
+ - id: log-beta
25438
+ label: Closed Beta & Bug Bounty
25439
+ - id: log-rollout
25440
+ label: Phased Public Rollout
25441
+ edges[0]:
25442
+ view:
25443
+ layout: radial-tree
25444
+ collapsedNodeIds[0]:
25445
+ embed_ref: mind-map-f95404
25446
+ status: finished
25447
+ node_count: 20
25448
+ edge_count: 0
25449
+ validation:
25450
+ status: valid
25451
+ warnings[0]:
25452
+ parse_error: null
25453
+ version_number: 1`,
25454
+ "parent_embed_id": null,
25455
+ "embed_ids": null
25456
+ }
25457
+ ],
25458
+ metadata: {
25459
+ featured: true,
25460
+ order: 105,
25461
+ content_embed_examples: ["mindmaps.mindmap"]
25462
+ }
25463
+ };
25464
+
24993
25465
  // ../ui/src/demo_chats/exampleChatData.ts
24994
25466
  var ALL_EXAMPLE_CHATS = [
24995
25467
  giganticAirplanesChat,
@@ -25077,7 +25549,8 @@ var ALL_EXAMPLE_CHATS = [
25077
25549
  rostockHeavyRainRadarChat,
25078
25550
  classicCarReverseImageSearchChat,
25079
25551
  usbC3v3LdoPcbSchematicChat,
25080
- berlinMorningBikeForecastChat
25552
+ berlinMorningBikeForecastChat,
25553
+ privacyFirstProductLaunchMindMapChat
25081
25554
  ].sort((a, b) => a.metadata.order - b.metadata.order);
25082
25555
 
25083
25556
  // ../ui/src/i18n/locales/en.json
@@ -27711,6 +28184,12 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
27711
28184
  continue: {
27712
28185
  text: "Continue"
27713
28186
  },
28187
+ skip: {
28188
+ text: "Skip"
28189
+ },
28190
+ select_interests: {
28191
+ text: "Select interests"
28192
+ },
27714
28193
  software_development: {
27715
28194
  text: "software development"
27716
28195
  },
@@ -27740,6 +28219,21 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
27740
28219
  },
27741
28220
  learn_anything: {
27742
28221
  text: "learn anything"
28222
+ },
28223
+ plan_trips: {
28224
+ text: "plan trips"
28225
+ },
28226
+ create_images: {
28227
+ text: "create images"
28228
+ },
28229
+ follow_news: {
28230
+ text: "follow news"
28231
+ },
28232
+ stay_healthy: {
28233
+ text: "stay healthy"
28234
+ },
28235
+ write_better: {
28236
+ text: "write better"
27743
28237
  }
27744
28238
  },
27745
28239
  new_chat: {
@@ -33210,6 +33704,38 @@ Like [Denis Villeneuve](wiki:Denis_Villeneuve)\u2019s work, *Contact* avoids the
33210
33704
  text: '```json\n{"type": "app_skill_use", "embed_id": "54a8d949-0a13-463f-bc6a-958e1f746f79", "app_id": "pdf", "skill_id": "view", "query": "Please describe the visual layout of this page, including the title and the structure of any bulleted lists."}\n```\n\n_\nThe first page of sample.pdf features a clean, professional layout with the following structure:\n\n### **Visual Layout Overview**\n* **Title:** The document begins with a prominent, bold heading: **"OpenMates PDF Test Document"**.\n* **Subheading:** Immediately below the title is a line indicating the page and section: "Page 1 of 2 \u2014 Introduction".\n* **Introductory Paragraph:** A brief paragraph explains that the document is intended for testing PDF upload and AI reading features.\n* **Bullet List Structure:** \n * The list is introduced by the phrase "Key facts on this page:".\n * It consists of **three bullet points**, each marked with a standard black circular bullet (\u2022).\n * The bullets highlight the document title, a "secret word" (ALPHA), and the nature of the text on the page.\n* **Closing Text:** A final sentence notes that an AI assistant should be able to read the content and identify the secret word.\n\nWould you like me to [read the text](/#settings/apps/pdf) from page 2 or [search for specific keywords](/#settings/apps/pdf) within the document?'
33211
33705
  }
33212
33706
  },
33707
+ privacy_first_product_launch_mind_map: {
33708
+ title: {
33709
+ text: "Privacy-First Product Launch Mind Map"
33710
+ },
33711
+ summary: {
33712
+ text: "A structured mind map for planning a privacy-first product launch across product, legal, marketing, UX, and logistics."
33713
+ },
33714
+ message_1: {
33715
+ text: "Create a visual mind map artifact for planning a privacy-first product launch. Use a central idea with clear branches and sub-branches."
33716
+ },
33717
+ message_2: {
33718
+ text: 'Here is a visual mind map to help you organize and plan your privacy-first product launch. I\'ve broken it down into key areas: Product/Engineering, Legal, Marketing, UX, and Logistics. \n\n```json\n{"type": "mindmap", "embed_id": "f9540460-1fce-441a-9abc-f627efaf1c0f"}\n```\n\n### Design Notes for a Privacy-First Launch\nSince privacy is deeply connected to user trust, your UI and UX design choices will be just as important as your backend engineering. When you\'re designing the interface (especially the **UX** branch of this map), keep these principles in mind:\n* **Visual Transparency:** Avoid hiding data toggles in complex sub-menus. A clean, spacious privacy dashboard gives users a sense of control. \n* **Plain Language Design:** Pair your "Plain-English Privacy Policy" with simple iconography. E.g., an open padlock vs. a closed padlock.\n* **Combine AI and Your Own Craft:** If you need placeholder icons, UI mockups, or wireframes for this launch, you can sketch them out on paper or in a simple tool, upload them here, and we can use AI to refine those rough concepts together. It\u2019s a great way to keep the personal touch while moving quickly!\n\nWhich area of the launch map feels like the biggest hurdle for you right now? I\'d be happy to help you brainstorm marketing assets or sketch out some UI flows for your consent screens!'
33719
+ },
33720
+ follow_up_1: {
33721
+ text: "Search for upcoming privacy-focused product launch events"
33722
+ },
33723
+ follow_up_2: {
33724
+ text: "Explain the importance of visual transparency in UI design"
33725
+ },
33726
+ follow_up_3: {
33727
+ text: "Find tutorials on designing privacy dashboards"
33728
+ },
33729
+ follow_up_4: {
33730
+ text: "List key principles for a privacy-first product launch"
33731
+ },
33732
+ follow_up_5: {
33733
+ text: "Generate mockups for a clean privacy dashboard"
33734
+ },
33735
+ follow_up_6: {
33736
+ text: "Help me brainstorm marketing assets for a privacy-focused product"
33737
+ }
33738
+ },
33213
33739
  privacy_website_hero_background: {
33214
33740
  title: {
33215
33741
  text: "Privacy Website Hero Background"
@@ -40105,6 +40631,33 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
40105
40631
  },
40106
40632
  finalize_error: {
40107
40633
  text: "Could not finish updating connected account."
40634
+ },
40635
+ cli_export_title: {
40636
+ text: "CLI test-account import"
40637
+ },
40638
+ cli_export_warning: {
40639
+ text: "This is for testing only. Anyone with the generated command and passcode can add this provider account to another OpenMates account."
40640
+ },
40641
+ cli_export_passcode_placeholder: {
40642
+ text: "Passcode for the CLI import"
40643
+ },
40644
+ cli_export_passcode_required: {
40645
+ text: "Enter a passcode before generating the CLI import command."
40646
+ },
40647
+ cli_export_button: {
40648
+ text: "Generate CLI import command"
40649
+ },
40650
+ cli_export_command_label: {
40651
+ text: "Generated CLI import command"
40652
+ },
40653
+ cli_export_copied: {
40654
+ text: "CLI import command copied. Paste it while logged into the test account in OpenMates CLI."
40655
+ },
40656
+ cli_export_generated: {
40657
+ text: "CLI import command generated. Copy it and paste it while logged into the test account in OpenMates CLI."
40658
+ },
40659
+ cli_export_error: {
40660
+ text: "Could not generate the CLI import command."
40108
40661
  }
40109
40662
  },
40110
40663
  nearby_by_default: {
@@ -43277,7 +43830,7 @@ function buildAssistantFeedbackDecision(rating) {
43277
43830
  }
43278
43831
 
43279
43832
  // src/benchmark.ts
43280
- import { randomUUID as randomUUID3 } from "crypto";
43833
+ import { randomUUID as randomUUID4 } from "crypto";
43281
43834
  import { existsSync as existsSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync6, readdirSync as readdirSync2, writeFileSync as writeFileSync4 } from "fs";
43282
43835
  import { tmpdir } from "os";
43283
43836
  import { dirname as dirname2, join as join4, resolve as resolve5 } from "path";
@@ -43577,7 +44130,7 @@ async function handleBenchmark(client, subcommand, rest, flags) {
43577
44130
  const caseIds = parseCaseIds(flags.case);
43578
44131
  const dryRun = flags["dry-run"] === true;
43579
44132
  const output = typeof flags.output === "string" ? flags.output : void 0;
43580
- const runId = typeof flags["run-id"] === "string" ? flags["run-id"] : randomUUID3();
44133
+ const runId = typeof flags["run-id"] === "string" ? flags["run-id"] : randomUUID4();
43581
44134
  const imagePath = typeof flags.image === "string" ? resolve5(flags.image) : defaultImageFixturePath();
43582
44135
  if (!dryRun && flags["confirm-spend-credits"] !== true) {
43583
44136
  throw new Error(
@@ -43987,7 +44540,7 @@ function buildLongContextHistory() {
43987
44540
  }
43988
44541
  function appendHistory(history, role, content) {
43989
44542
  history.push({
43990
- message_id: randomUUID3(),
44543
+ message_id: randomUUID4(),
43991
44544
  role,
43992
44545
  sender_name: role === "user" ? "User" : "Assistant",
43993
44546
  content,
@@ -44326,6 +44879,10 @@ async function main() {
44326
44879
  printSettingsHelp(client);
44327
44880
  return;
44328
44881
  }
44882
+ if (command === "connected-accounts") {
44883
+ printConnectedAccountsHelp();
44884
+ return;
44885
+ }
44329
44886
  if (command === "learning-mode") {
44330
44887
  printLearningModeHelp();
44331
44888
  return;
@@ -44428,6 +44985,10 @@ async function main() {
44428
44985
  await handleSettings(client, subcommand, rest, parsed.flags);
44429
44986
  return;
44430
44987
  }
44988
+ if (command === "connected-accounts") {
44989
+ await handleConnectedAccounts(client, subcommand, parsed.flags);
44990
+ return;
44991
+ }
44431
44992
  if (command === "learning-mode") {
44432
44993
  await handleLearningMode(client, subcommand, parsed.flags);
44433
44994
  return;
@@ -46152,6 +46713,43 @@ async function promptSecret(question) {
46152
46713
  stdin2.on("data", onData);
46153
46714
  });
46154
46715
  }
46716
+ async function handleConnectedAccounts(client, subcommand, flags) {
46717
+ if (!subcommand || subcommand === "help" || flags.help === true) {
46718
+ printConnectedAccountsHelp();
46719
+ return;
46720
+ }
46721
+ if (subcommand !== "import") {
46722
+ throw new Error(`Unknown connected-accounts command '${subcommand}'. Run 'openmates connected-accounts --help'.`);
46723
+ }
46724
+ if (flags.passcode !== void 0) {
46725
+ throw new Error("Connected account import passcodes must be entered through the hidden prompt, not a command-line flag.");
46726
+ }
46727
+ const payload = typeof flags.payload === "string" ? flags.payload.trim() : "";
46728
+ if (!payload) {
46729
+ throw new Error("Missing --payload. Paste the command generated by connected account settings.");
46730
+ }
46731
+ if (!client.hasSession()) {
46732
+ throw new Error("Not logged in. Run `openmates login` before importing a connected account.");
46733
+ }
46734
+ const passcode = await promptSecret("Connected account import passcode: ");
46735
+ const result = await client.importConnectedAccountFromCliPayload({
46736
+ encryptedPayload: payload,
46737
+ passcode
46738
+ });
46739
+ if (flags.json === true) {
46740
+ printJson2({
46741
+ id: result.id,
46742
+ provider_id: result.providerId,
46743
+ app_id: result.appId,
46744
+ validation: result.validation
46745
+ });
46746
+ return;
46747
+ }
46748
+ console.log("Connected account imported.");
46749
+ console.log(`Provider: ${result.providerId}`);
46750
+ console.log(`App: ${result.appId}`);
46751
+ console.log("Validation: harmless read succeeded");
46752
+ }
46155
46753
  async function writeSecretFile(filePath, content, force = false) {
46156
46754
  const { mkdir, writeFile, stat: stat2 } = await import("fs/promises");
46157
46755
  const { dirname: dirname4 } = await import("path");
@@ -48891,6 +49489,7 @@ Commands:
48891
49489
  openmates mentions [--help] List available @mentions
48892
49490
  openmates embeds [--help] Embed commands (show)
48893
49491
  openmates settings [--help] Predefined settings commands
49492
+ openmates connected-accounts [--help] Connected account import helpers
48894
49493
  openmates learning-mode [--help] Account-wide Learning Mode controls
48895
49494
  openmates inspirations [--lang <code>] [--json] Daily inspirations
48896
49495
  openmates newchatsuggestions [--limit <n>] [--json] Personalized new chat suggestions
@@ -48906,6 +49505,22 @@ Flags:
48906
49505
  --api-key <key> Optional API key override (or set OPENMATES_API_KEY)
48907
49506
  --help Show contextual help for any command`);
48908
49507
  }
49508
+ function printConnectedAccountsHelp() {
49509
+ console.log(`Connected account commands:
49510
+ openmates connected-accounts import --payload <OMCA1...> [--json]
49511
+
49512
+ Imports one passcode-protected connected account generated from web settings.
49513
+ The CLI prompts for the passcode interactively, validates the provider token with
49514
+ a harmless read, then re-encrypts the account for the currently logged-in CLI
49515
+ account before storing it.
49516
+
49517
+ Options:
49518
+ --payload <OMCA1...> Required encrypted import payload from web settings
49519
+ --json Output a redacted JSON summary
49520
+
49521
+ Security:
49522
+ Do not pass the passcode as a flag. It is always entered through a hidden prompt.`);
49523
+ }
48909
49524
  function printFeedbackHelp() {
48910
49525
  console.log(`Feedback commands:
48911
49526
  openmates feedback assistant-response --rating <1-5> [--json]
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  getExtForLang,
4
4
  serializeToYaml
5
- } from "./chunk-4S3EN6KA.js";
5
+ } from "./chunk-FCSXUBOU.js";
6
6
  import "./chunk-AXNRPVLE.js";
7
7
  export {
8
8
  getExtForLang,
package/dist/index.d.ts CHANGED
@@ -209,6 +209,18 @@ interface EmbedKeyWrapper {
209
209
  */
210
210
  type ShareDuration = 0 | 60 | 3600 | 86400 | 604800 | 1209600 | 2592000 | 7776000;
211
211
 
212
+ interface ConnectedAccountCliTransferPayload {
213
+ version: 1;
214
+ provider_id: string;
215
+ app_id: string;
216
+ label: string;
217
+ account_ref?: string;
218
+ capabilities: string[];
219
+ runtime_modes: Record<string, string>;
220
+ refresh_token_bundle: Record<string, unknown>;
221
+ created_at: string;
222
+ }
223
+
212
224
  interface CliSubChatRequest {
213
225
  id?: string;
214
226
  chat_id?: string;
@@ -249,6 +261,19 @@ interface ConnectedAccountTurnTokenRef {
249
261
  action_scope?: Record<string, unknown>;
250
262
  expires_at: number;
251
263
  }
264
+ interface ConnectedAccountImportValidationResult {
265
+ valid: boolean;
266
+ provider_id: string;
267
+ app_id: string;
268
+ checked_at: number;
269
+ }
270
+ interface ConnectedAccountImportResult {
271
+ id: string;
272
+ providerId: string;
273
+ appId: string;
274
+ label: string;
275
+ validation: ConnectedAccountImportValidationResult;
276
+ }
252
277
  type LearningModeAgeGroup = "under_10" | "10_12" | "13_15" | "16_18" | "adult";
253
278
  interface LearningModeStatus {
254
279
  enabled: boolean;
@@ -565,6 +590,12 @@ declare class OpenMatesClient {
565
590
  messageId: string;
566
591
  refs: ConnectedAccountTurnTokenRefInput[];
567
592
  }): Promise<ConnectedAccountTurnTokenRef[]>;
593
+ importConnectedAccountFromCliPayload(params: {
594
+ encryptedPayload: string;
595
+ passcode: string;
596
+ }): Promise<ConnectedAccountImportResult>;
597
+ validateConnectedAccountImportPayload(payload: ConnectedAccountCliTransferPayload): Promise<ConnectedAccountImportValidationResult>;
598
+ private createConnectedAccountImportRow;
568
599
  cancelConnectedAccountAction(params: {
569
600
  actionId: string;
570
601
  chatId: string;
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  getExtForLang,
12
12
  normalizeInterestTagIds,
13
13
  serializeToYaml
14
- } from "./chunk-4S3EN6KA.js";
14
+ } from "./chunk-FCSXUBOU.js";
15
15
  import "./chunk-AXNRPVLE.js";
16
16
  export {
17
17
  ASSISTANT_FEEDBACK_REPORT_TITLE,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openmates",
3
- "version": "0.12.0-alpha.27",
3
+ "version": "0.12.0-alpha.29",
4
4
  "description": "OpenMates CLI and SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -30,8 +30,9 @@
30
30
  "test:unit:billing": "node --test tests/billing.test.ts",
31
31
  "test:unit:account-delete": "node --test tests/account-delete.test.ts",
32
32
  "test:unit:cli": "node --test tests/cli.test.ts",
33
+ "test:unit:connected-account-import": "node --test --experimental-strip-types --loader ./tests/loader.mjs tests/connectedAccountImport.test.ts",
33
34
  "test:unit:benchmark": "node --test --experimental-strip-types --loader ./tests/loader.mjs src/__tests__/benchmark.test.ts",
34
- "test": "node --test --experimental-strip-types --loader ./tests/loader.mjs tests/crypto.test.ts tests/storage.test.ts tests/keychain.test.ts tests/mentions.test.ts tests/outputRedactor.test.ts tests/fileEmbed.test.ts tests/embedCreator.test.ts tests/shareEncryption.test.ts tests/server.test.ts tests/ws.test.ts tests/urlEmbed.test.ts src/__tests__/benchmark.test.ts && node --test --experimental-strip-types tests/embedRenderers.test.ts && node --test tests/cli.test.ts tests/billing.test.ts tests/account-delete.test.ts tests/signup.test.ts tests/security-setup.test.ts tests/e2e-provisioning.test.ts"
35
+ "test": "node --test --experimental-strip-types --loader ./tests/loader.mjs tests/crypto.test.ts tests/storage.test.ts tests/keychain.test.ts tests/mentions.test.ts tests/outputRedactor.test.ts tests/fileEmbed.test.ts tests/embedCreator.test.ts tests/shareEncryption.test.ts tests/server.test.ts tests/ws.test.ts tests/urlEmbed.test.ts src/__tests__/benchmark.test.ts && node --test --experimental-strip-types --loader ./tests/loader.mjs tests/embedRenderers.test.ts tests/connectedAccountImport.test.ts && node --test tests/cli.test.ts tests/billing.test.ts tests/account-delete.test.ts tests/signup.test.ts tests/security-setup.test.ts tests/e2e-provisioning.test.ts"
35
36
  },
36
37
  "keywords": [
37
38
  "openmates",