@lumerahq/cli 0.23.0-dev.2 → 0.23.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.
@@ -41,6 +41,24 @@ function resolveVersion(cwd) {
41
41
  }
42
42
  return formatTimestampVersion();
43
43
  }
44
+ function resolveUIVersion(cwd) {
45
+ try {
46
+ const installed = JSON.parse(
47
+ readFileSync(resolve(cwd, "node_modules/@lumerahq/ui/package.json"), "utf-8")
48
+ );
49
+ if (installed?.version) return String(installed.version);
50
+ } catch {
51
+ }
52
+ try {
53
+ const pkg = JSON.parse(readFileSync(resolve(cwd, "package.json"), "utf-8"));
54
+ const spec = pkg?.dependencies?.["@lumerahq/ui"] ?? pkg?.devDependencies?.["@lumerahq/ui"];
55
+ if (!spec) return "none";
56
+ const cleaned = String(spec).replace(/^[\^~>=<\s]+/, "");
57
+ return /^\d+\.\d+/.test(cleaned) ? cleaned : "";
58
+ } catch {
59
+ return "";
60
+ }
61
+ }
44
62
  function resolveGitSha(cwd) {
45
63
  try {
46
64
  return execSync("git rev-parse HEAD", {
@@ -62,11 +80,14 @@ async function createTarball(distDir) {
62
80
  archive.finalize();
63
81
  });
64
82
  }
65
- async function ensureAppRecord(apiBase, token, appName, appTitle) {
83
+ async function ensureAppRecord(apiBase, token, appName, appTitle, accessLevel) {
66
84
  const appData = {
67
85
  name: appTitle,
68
86
  hosting_type: "s3"
69
87
  };
88
+ if (accessLevel) {
89
+ appData.access_level = accessLevel;
90
+ }
70
91
  const res = await fetchWithRetry(`${apiBase}/custom-apps/${encodeURIComponent(appName)}/registration`, {
71
92
  method: "PUT",
72
93
  headers: {
@@ -87,13 +108,14 @@ async function deploy(options) {
87
108
  appName,
88
109
  appTitle,
89
110
  distDir,
90
- apiUrl
111
+ apiUrl,
112
+ accessLevel
91
113
  } = options;
92
114
  if (!existsSync(distDir)) {
93
115
  throw new Error(`dist directory not found: ${distDir}`);
94
116
  }
95
117
  console.log(pc.dim("Ensuring app record..."));
96
- await ensureAppRecord(apiUrl, token, appName, appTitle);
118
+ await ensureAppRecord(apiUrl, token, appName, appTitle, accessLevel);
97
119
  console.log(pc.cyan(`Deploying ${appTitle}...`));
98
120
  const version = options.version || process.env.LUMERA_APP_VERSION?.trim() || resolveVersion(dirname(distDir));
99
121
  console.log(pc.dim(`Version: ${version}`));
@@ -104,6 +126,8 @@ async function deploy(options) {
104
126
  formData.append("version", version);
105
127
  const gitSha = resolveGitSha(dirname(distDir));
106
128
  if (gitSha) formData.append("git_sha", gitSha);
129
+ const uiVersion = resolveUIVersion(dirname(distDir));
130
+ if (uiVersion) formData.append("ui_version", uiVersion);
107
131
  const res = await fetchWithRetry(`${apiUrl}/custom-apps/${appName}/deploy`, {
108
132
  method: "POST",
109
133
  headers: {
@@ -118,9 +142,13 @@ async function deploy(options) {
118
142
  }
119
143
  const result = await res.json();
120
144
  const launchUrl = result.launch_url;
145
+ const appUrl = result.app_url;
121
146
  console.log(pc.green(`
122
147
  Deployed! ${launchUrl}`));
123
- return { launchUrl, version };
148
+ if (appUrl) {
149
+ console.log(pc.green(`App URL: ${appUrl}`));
150
+ }
151
+ return { launchUrl, appUrl, version };
124
152
  }
125
153
  async function registerDevApp(apiBase, token, appName, appTitle, appUrl) {
126
154
  const appData = {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  dev
3
- } from "./chunk-WV5HOQNX.js";
3
+ } from "./chunk-I7QEYKS7.js";
4
4
  import {
5
5
  syncDeps
6
6
  } from "./chunk-3ESHIPZH.js";
package/dist/index.js CHANGED
@@ -226,29 +226,29 @@ async function main() {
226
226
  switch (command) {
227
227
  // Resource commands
228
228
  case "plan":
229
- await import("./resources-7243WXIF.js").then((m) => m.plan(args.slice(1)));
229
+ await import("./resources-AMXL4A64.js").then((m) => m.plan(args.slice(1)));
230
230
  break;
231
231
  case "apply":
232
- await import("./resources-7243WXIF.js").then((m) => m.apply(args.slice(1)));
232
+ await import("./resources-AMXL4A64.js").then((m) => m.apply(args.slice(1)));
233
233
  break;
234
234
  case "pull":
235
- await import("./resources-7243WXIF.js").then((m) => m.pull(args.slice(1)));
235
+ await import("./resources-AMXL4A64.js").then((m) => m.pull(args.slice(1)));
236
236
  break;
237
237
  case "destroy":
238
- await import("./resources-7243WXIF.js").then((m) => m.destroy(args.slice(1)));
238
+ await import("./resources-AMXL4A64.js").then((m) => m.destroy(args.slice(1)));
239
239
  break;
240
240
  case "list":
241
- await import("./resources-7243WXIF.js").then((m) => m.list(args.slice(1)));
241
+ await import("./resources-AMXL4A64.js").then((m) => m.list(args.slice(1)));
242
242
  break;
243
243
  case "show":
244
- await import("./resources-7243WXIF.js").then((m) => m.show(args.slice(1)));
244
+ await import("./resources-AMXL4A64.js").then((m) => m.show(args.slice(1)));
245
245
  break;
246
246
  case "diff":
247
- await import("./resources-7243WXIF.js").then((m) => m.diff(args.slice(1)));
247
+ await import("./resources-AMXL4A64.js").then((m) => m.diff(args.slice(1)));
248
248
  break;
249
249
  // Development
250
250
  case "dev":
251
- await import("./dev-UTPD4MN5.js").then((m) => m.dev(args.slice(1)));
251
+ await import("./dev-VWHFYKGC.js").then((m) => m.dev(args.slice(1)));
252
252
  break;
253
253
  case "run":
254
254
  await import("./run-DJXAXDD2.js").then((m) => m.run(args.slice(1)));
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  deploy
3
- } from "./chunk-WV5HOQNX.js";
3
+ } from "./chunk-I7QEYKS7.js";
4
4
  import {
5
5
  syncDeps
6
6
  } from "./chunk-3ESHIPZH.js";
@@ -31,7 +31,7 @@ import pc2 from "picocolors";
31
31
  import prompts from "prompts";
32
32
  import { execFileSync, execSync } from "child_process";
33
33
  import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
34
- import { join as join2, basename as basename2, resolve } from "path";
34
+ import { join as join2, resolve } from "path";
35
35
 
36
36
  // src/lib/lint/index.ts
37
37
  import { existsSync, readFileSync, readdirSync } from "fs";
@@ -124,7 +124,6 @@ var collectionSchemaRule = {
124
124
  const id = parsed.id;
125
125
  const name = parsed.name;
126
126
  const fields = parsed.fields;
127
- const audit = parsed.audit;
128
127
  const indexes = parsed.indexes;
129
128
  const search = parsed.search;
130
129
  const localFieldTypes = /* @__PURE__ */ new Map();
@@ -164,20 +163,8 @@ var collectionSchemaRule = {
164
163
  if (typeof field.type !== "string" || field.type.trim() === "") {
165
164
  issues.push(error(target, `Field ${typeof field.name === "string" ? `"${field.name}"` : `at index ${i}`} is missing required string field "type".`));
166
165
  }
167
- const fieldLabel = typeof field.name === "string" ? `"${field.name}"` : `at index ${i}`;
168
- if (field.type === "select") {
169
- if (field.options !== void 0) {
170
- issues.push(error(target, `Select field ${fieldLabel} must define choices with top-level "values", not "options.values".`));
171
- }
172
- if (!Array.isArray(field.values) || field.values.length === 0 || !field.values.every((value) => typeof value === "string" && value.trim() !== "")) {
173
- issues.push(error(target, `Select field ${fieldLabel} must include a non-empty string array "values".`));
174
- }
175
- }
176
166
  }
177
167
  }
178
- if (audit !== void 0 && typeof audit !== "boolean") {
179
- issues.push(error(target, 'Optional field "audit" must be a boolean when present.'));
180
- }
181
168
  if (indexes !== void 0) {
182
169
  if (!Array.isArray(indexes)) {
183
170
  issues.push(error(target, 'Optional field "indexes" must be an array when present.'));
@@ -906,11 +893,6 @@ function stripNamespacePrefix(name, appName) {
906
893
  }
907
894
  return name;
908
895
  }
909
- function namespacedCollectionID(id, appName) {
910
- if (!appName) return id;
911
- const prefix = sanitizeSlugForCollectionName(appName) + NAMESPACE_SEPARATOR;
912
- return id.startsWith(prefix) ? id : prefix + id;
913
- }
914
896
  var AGENT_IDLE_REAP_TIMEOUT_MIN_SECONDS = 10;
915
897
  var AGENT_IDLE_REAP_TIMEOUT_MAX_SECONDS = 900;
916
898
  function buildCollectionIdMap(collections, appName) {
@@ -922,7 +904,6 @@ function buildCollectionIdMap(collections, appName) {
922
904
  for (const collection of collections) {
923
905
  add(collection.id, collection.id);
924
906
  add(collection.name, collection.id);
925
- add(stripNamespacePrefix(collection.id, appName), collection.id);
926
907
  const stripped = stripNamespacePrefix(collection.name, appName);
927
908
  add(stripped, collection.id);
928
909
  if (collection.name.includes(NAMESPACE_SEPARATOR)) {
@@ -1385,10 +1366,6 @@ function loadLocalCollections(platformDir, filterName) {
1385
1366
  errors.push(`${file}: missing fields array`);
1386
1367
  continue;
1387
1368
  }
1388
- if (collection.audit !== void 0 && typeof collection.audit !== "boolean") {
1389
- errors.push(`${file}: audit must be a boolean when present`);
1390
- continue;
1391
- }
1392
1369
  for (let i = 0; i < collection.fields.length; i++) {
1393
1370
  const field = collection.fields[i];
1394
1371
  if (!field || typeof field !== "object") {
@@ -1565,22 +1542,15 @@ function convertCollectionToApiFormat(local) {
1565
1542
  const indexName = `idx_${local.id}_${idx.fields.join("_")}`;
1566
1543
  return `CREATE ${unique}INDEX ${indexName} ON ${local.name} (${fields})`;
1567
1544
  });
1568
- return {
1569
- id: local.id,
1570
- name: local.name,
1571
- schema,
1572
- indexes,
1573
- ...local.audit !== void 0 ? { audit: local.audit } : {},
1574
- search: local.search ?? null
1575
- };
1545
+ return { id: local.id, name: local.name, schema, indexes, search: local.search ?? null };
1576
1546
  }
1577
1547
  function mapFieldType(type) {
1578
1548
  const typeMap = {
1579
1549
  text: "text",
1580
1550
  number: "number",
1581
1551
  bool: "bool",
1582
- email: "email",
1583
- url: "url",
1552
+ email: "text",
1553
+ url: "text",
1584
1554
  date: "date",
1585
1555
  select: "select",
1586
1556
  relation: "relation",
@@ -1601,7 +1571,7 @@ function normalizeConstraintValue(fieldType, value) {
1601
1571
  }
1602
1572
  return value;
1603
1573
  }
1604
- function fieldsDiffer(local, remote, appName) {
1574
+ function fieldsDiffer(local, remote) {
1605
1575
  if (mapFieldType(local.type) !== remote.type) return true;
1606
1576
  if ((local.required || false) !== (remote.required || false)) return true;
1607
1577
  const opts = remote.options || {};
@@ -1615,8 +1585,7 @@ function fieldsDiffer(local, remote, appName) {
1615
1585
  if (!localMultiple && remoteMaxSelect > 1) return true;
1616
1586
  }
1617
1587
  if (local.type === "relation") {
1618
- const remoteCollection = stripNamespacePrefix(opts.collectionId || "", appName);
1619
- if (local.collection && local.collection !== remoteCollection) return true;
1588
+ if (local.collection && local.collection !== opts.collectionId) return true;
1620
1589
  const localMultiple = local.multiple || false;
1621
1590
  const remoteMaxSelect = opts.maxSelect || 1;
1622
1591
  if (localMultiple && remoteMaxSelect <= 1) return true;
@@ -1683,22 +1652,19 @@ function normalizeCollectionSearch(search) {
1683
1652
  } : {}
1684
1653
  });
1685
1654
  }
1686
- async function planCollections(api, localCollections, appName) {
1655
+ async function planCollections(api, localCollections) {
1687
1656
  const changes = [];
1688
1657
  const remoteCollections = await api.listCollections();
1689
1658
  const remoteById = new Map(remoteCollections.map((c) => [c.id, c]));
1690
- const remoteByLocalName = new Map(remoteCollections.map((c) => [stripNamespacePrefix(c.name, appName), c]));
1691
1659
  for (const local of localCollections) {
1692
- const remote = remoteByLocalName.get(local.name) ?? remoteById.get(namespacedCollectionID(local.id, appName)) ?? (!appName ? remoteById.get(local.id) : void 0);
1660
+ const remote = remoteById.get(local.id);
1693
1661
  if (!remote) {
1694
- const details = [`${local.fields.length} fields`];
1695
- if (local.audit) details.push("record audit enabled");
1696
1662
  changes.push({
1697
1663
  type: "create",
1698
1664
  resource: "collection",
1699
1665
  id: local.id,
1700
1666
  name: local.name,
1701
- details: details.join(", "),
1667
+ details: `${local.fields.length} fields`,
1702
1668
  fieldDetails: local.fields.map((f) => ({
1703
1669
  action: "+",
1704
1670
  name: f.name,
@@ -1718,21 +1684,19 @@ async function planCollections(api, localCollections, appName) {
1718
1684
  if (!remoteFieldNames.has(name)) continue;
1719
1685
  const localField = localFieldMap.get(name);
1720
1686
  const remoteField = remoteFieldMap.get(name);
1721
- if (fieldsDiffer(localField, remoteField, appName)) {
1687
+ if (fieldsDiffer(localField, remoteField)) {
1722
1688
  modified.push(name);
1723
1689
  }
1724
1690
  }
1725
1691
  const indexesChanged = indexesDiffer(local, remote.indexes || []);
1726
1692
  const searchChanged = normalizeCollectionSearch(local.search) !== normalizeCollectionSearch(remote.search);
1727
- const auditChanged = local.audit !== void 0 && local.audit !== remote.audit;
1728
- if (added.length > 0 || removed.length > 0 || modified.length > 0 || indexesChanged || searchChanged || auditChanged) {
1693
+ if (added.length > 0 || removed.length > 0 || modified.length > 0 || indexesChanged || searchChanged) {
1729
1694
  const details = [];
1730
1695
  if (added.length > 0) details.push(`+${added.length} field${added.length > 1 ? "s" : ""}`);
1731
1696
  if (removed.length > 0) details.push(`-${removed.length} field${removed.length > 1 ? "s" : ""}`);
1732
1697
  if (modified.length > 0) details.push(`~${modified.length} field${modified.length > 1 ? "s" : ""} (${modified.join(", ")})`);
1733
1698
  if (indexesChanged) details.push("indexes changed");
1734
1699
  if (searchChanged) details.push("search changed");
1735
- if (auditChanged) details.push(local.audit ? "record audit enabled" : "record audit disabled");
1736
1700
  const fieldDetails = [];
1737
1701
  for (const name of added) {
1738
1702
  const f = localFieldMap.get(name);
@@ -2200,23 +2164,25 @@ async function applyApp(args) {
2200
2164
  }
2201
2165
  }
2202
2166
  const distDir = resolve(projectRoot, "dist");
2203
- await deploy({ token, appName, appTitle, distDir, apiUrl });
2167
+ const accessLevel = getFlagValue(args, "access-level");
2168
+ const validAccessLevels = [
2169
+ "org_members_with_permission",
2170
+ "org_members",
2171
+ "invited",
2172
+ "signed_in",
2173
+ "public"
2174
+ ];
2175
+ if (accessLevel && !validAccessLevels.includes(accessLevel)) {
2176
+ throw new Error(
2177
+ `Invalid --access-level "${accessLevel}". Valid values: ${validAccessLevels.join(", ")}`
2178
+ );
2179
+ }
2180
+ await deploy({ token, appName, appTitle, distDir, apiUrl, accessLevel });
2204
2181
  }
2205
2182
  async function pullCollections(api, platformDir, filterName, appName) {
2206
2183
  const collectionsDir = join2(platformDir, "collections");
2207
2184
  mkdirSync(collectionsDir, { recursive: true });
2208
2185
  const collections = await api.listCollections();
2209
- const existingFiles = /* @__PURE__ */ new Map();
2210
- for (const file of readdirSync2(collectionsDir)) {
2211
- if (!file.endsWith(".json")) continue;
2212
- const filePath = join2(collectionsDir, file);
2213
- try {
2214
- const existing = JSON.parse(readFileSync2(filePath, "utf-8"));
2215
- if (typeof existing.id === "string") existingFiles.set(existing.id, filePath);
2216
- if (typeof existing.name === "string") existingFiles.set(existing.name, filePath);
2217
- } catch {
2218
- }
2219
- }
2220
2186
  for (const collection of collections) {
2221
2187
  if (collection.system || collection.managed) continue;
2222
2188
  if (appName && collection.name.includes(NAMESPACE_SEPARATOR)) {
@@ -2224,14 +2190,12 @@ async function pullCollections(api, platformDir, filterName, appName) {
2224
2190
  if (prefix !== sanitizeSlugForCollectionName(appName)) continue;
2225
2191
  }
2226
2192
  const localName = stripNamespacePrefix(collection.name, appName);
2227
- const localID = stripNamespacePrefix(collection.id, appName);
2228
- if (filterName && localName !== filterName && localID !== filterName && collection.name !== filterName && collection.id !== filterName) {
2193
+ if (filterName && localName !== filterName && collection.name !== filterName && collection.id !== filterName) {
2229
2194
  continue;
2230
2195
  }
2231
2196
  const localFormat = {
2232
- id: localID,
2197
+ id: collection.id,
2233
2198
  name: localName,
2234
- audit: collection.audit,
2235
2199
  fields: collection.schema.map((field) => {
2236
2200
  const localField = {
2237
2201
  name: field.name,
@@ -2240,9 +2204,7 @@ async function pullCollections(api, platformDir, filterName, appName) {
2240
2204
  };
2241
2205
  if (field.options) {
2242
2206
  if (field.options.values) localField.values = field.options.values;
2243
- if (field.options.collectionId) {
2244
- localField.collection = stripNamespacePrefix(field.options.collectionId, appName);
2245
- }
2207
+ if (field.options.collectionId) localField.collection = field.options.collectionId;
2246
2208
  if (field.options.maxSelect && field.options.maxSelect > 1) localField.multiple = true;
2247
2209
  if (field.options.defaultCurrency) {
2248
2210
  localField.defaultCurrency = field.options.defaultCurrency;
@@ -2262,10 +2224,10 @@ async function pullCollections(api, platformDir, filterName, appName) {
2262
2224
  }).filter((idx) => idx !== null),
2263
2225
  search: collection.search
2264
2226
  };
2265
- const fileName = `${toSafeFilename(localName)}.json`;
2266
- const filePath = existingFiles.get(localFormat.id) || existingFiles.get(localFormat.name) || join2(collectionsDir, fileName);
2227
+ const fileName = toSafeFilename(localName);
2228
+ const filePath = join2(collectionsDir, `${fileName}.json`);
2267
2229
  writeFileSync(filePath, JSON.stringify(localFormat, null, 2) + "\n");
2268
- console.log(pc2.green(" \u2713"), `${localName} \u2192 collections/${basename2(filePath)}`);
2230
+ console.log(pc2.green(" \u2713"), `${localName} \u2192 collections/${fileName}.json`);
2269
2231
  }
2270
2232
  }
2271
2233
  async function pullAutomations(api, platformDir, filterName, projectId) {
@@ -2850,10 +2812,9 @@ async function destroyResources(api, platformDir, resourceType, resourceName, sk
2850
2812
  if (!resourceType || resourceType === "collections") {
2851
2813
  const localCollections = loadLocalCollections(platformDir, resourceName || void 0);
2852
2814
  const remoteCollections = await api.listCollections();
2853
- const remoteById = new Map(remoteCollections.map((c) => [c.id, c]));
2854
- const remoteByLocalName = new Map(remoteCollections.map((c) => [stripNamespacePrefix(c.name, appName), c]));
2815
+ const remoteByName = new Map(remoteCollections.map((c) => [c.name, c]));
2855
2816
  for (const local of localCollections) {
2856
- const remote = remoteByLocalName.get(local.name) ?? remoteById.get(namespacedCollectionID(local.id, appName)) ?? (!appName ? remoteById.get(local.id) : void 0);
2817
+ const remote = remoteByName.get(local.name);
2857
2818
  if (remote) {
2858
2819
  toDelete.push({ type: "collection", id: local.id, name: local.name, remoteId: remote.id });
2859
2820
  }
@@ -3056,9 +3017,7 @@ async function showResource(api, platformDir, resourceType, resourceName, appNam
3056
3017
  const localCollections = loadLocalCollections(platformDir, resourceName);
3057
3018
  const remoteCollections = await api.listCollections();
3058
3019
  const local = localCollections[0];
3059
- const remote = remoteCollections.find(
3060
- (collection) => stripNamespacePrefix(collection.name, appName) === resourceName || stripNamespacePrefix(collection.id, appName) === resourceName
3061
- );
3020
+ const remote = remoteCollections.find((c) => c.name === resourceName || c.id === resourceName);
3062
3021
  if (!local && !remote) {
3063
3022
  console.log(pc2.red(` Collection "${resourceName}" not found`));
3064
3023
  process.exit(1);
@@ -3289,7 +3248,7 @@ async function plan(args) {
3289
3248
  if (!type || type === "collections") {
3290
3249
  const localCollections = loadLocalCollections(platformDir, name || void 0);
3291
3250
  if (localCollections.length > 0) {
3292
- const changes = await planCollections(api, localCollections, appName);
3251
+ const changes = await planCollections(api, localCollections);
3293
3252
  allChanges.push(...changes);
3294
3253
  }
3295
3254
  }
@@ -3436,7 +3395,7 @@ async function apply(args) {
3436
3395
  const localHooks = !type || type === "hooks" ? loadLocalHooks(platformDir, name || void 0, appName) : [];
3437
3396
  const localAgents = !type || type === "agents" ? loadLocalAgents(platformDir, name || void 0, appName) : [];
3438
3397
  const localMailboxes = !type || type === "mailboxes" ? loadLocalMailboxes(platformDir, name || void 0) : [];
3439
- if (localCollections.length > 0) allChanges.push(...await planCollections(api, localCollections, appName));
3398
+ if (localCollections.length > 0) allChanges.push(...await planCollections(api, localCollections));
3440
3399
  if (localAutomations.length > 0) allChanges.push(...await planAutomations(api, localAutomations, projectId));
3441
3400
  if (localHooks.length > 0) allChanges.push(...await planHooks(api, localHooks, collections, projectId));
3442
3401
  if (localAgents.length > 0) allChanges.push(...await planAgents(api, localAgents, projectId));
@@ -3958,15 +3917,10 @@ async function diff(args) {
3958
3917
  }
3959
3918
  export {
3960
3919
  apply,
3961
- applyCollections,
3962
- convertCollectionToApiFormat,
3963
3920
  destroy,
3964
- destroyResources,
3965
3921
  diff,
3966
3922
  list,
3967
3923
  plan,
3968
- planCollections,
3969
3924
  pull,
3970
- pullCollections,
3971
3925
  show
3972
3926
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumerahq/cli",
3
- "version": "0.23.0-dev.2",
3
+ "version": "0.23.0",
4
4
  "description": "CLI for building and deploying Lumera apps",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,4 +1,10 @@
1
- import { type HostPayload, isEmbedded, onInitMessage, postReadyMessage } from '@lumerahq/ui/lib';
1
+ import {
2
+ type HostPayload,
3
+ isEmbedded,
4
+ onInitMessage,
5
+ postReadyMessage,
6
+ redirectToLogin,
7
+ } from '@lumerahq/ui/lib';
2
8
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3
9
  import { createHashHistory, createRouter, RouterProvider } from '@tanstack/react-router';
4
10
  import { StrictMode, useEffect, useRef, useState } from 'react';
@@ -60,24 +66,27 @@ function RouteRestorer() {
60
66
 
61
67
  const App = () => {
62
68
  const [hostContext, setHostContext] = useState<AuthContextValue | null>(null);
63
- const [status, setStatus] = useState<'pending' | 'ready' | 'denied'>('pending');
69
+ const [status, setStatus] = useState<'pending' | 'ready' | 'unauthenticated'>('pending');
64
70
 
65
71
  useEffect(() => {
66
72
  if (typeof window === 'undefined') return;
67
73
 
68
- const standaloneDevMode =
69
- import.meta.env.DEV && !isEmbedded() && !!import.meta.env.VITE_DEV_API_BASE_URL;
70
-
71
- if (!isEmbedded() && !standaloneDevMode) {
72
- setStatus('denied');
73
- return;
74
- }
75
-
74
+ // The bridge picks the transport automatically: postMessage when embedded
75
+ // in the Lumera shell, same-origin /api with the app-session cookie when
76
+ // served standalone on the app's own hostname, and direct dev fetch when
77
+ // VITE_DEV_API_BASE_URL is set.
76
78
  const cleanup = onInitMessage((payload?: HostPayload) => {
79
+ if (!payload) {
80
+ // No identity and no app context: authentication is required
81
+ // (payload.user alone being undefined means an anonymous visitor on
82
+ // a public app — that still counts as ready).
83
+ setStatus('unauthenticated');
84
+ return;
85
+ }
77
86
  setHostContext({
78
- company: payload?.company,
79
- user: payload?.user,
80
- sessionToken: payload?.session?.token,
87
+ company: payload.company,
88
+ user: payload.user,
89
+ sessionToken: payload.session?.token,
81
90
  });
82
91
  setStatus('ready');
83
92
  });
@@ -99,16 +108,23 @@ const App = () => {
99
108
  );
100
109
  }
101
110
 
102
- if (status === 'denied' || !hostContext) {
111
+ if (status === 'unauthenticated' || !hostContext) {
103
112
  return (
104
113
  <main className="flex min-h-screen items-center justify-center bg-background">
105
114
  <div className="text-center space-y-3">
106
115
  <div className="inline-flex size-12 rounded-xl bg-muted items-center justify-center mb-2">
107
- <span className="text-2xl font-semibold text-muted-foreground">403</span>
116
+ <span className="text-2xl font-semibold text-muted-foreground">401</span>
108
117
  </div>
109
- <p className="text-sm text-muted-foreground">
110
- Access denied, this app can only be accessed from within Lumera.
111
- </p>
118
+ <p className="text-sm text-muted-foreground">Sign in to access this app.</p>
119
+ {!isEmbedded() && (
120
+ <button
121
+ type="button"
122
+ className="text-sm font-medium text-primary underline underline-offset-4"
123
+ onClick={() => redirectToLogin()}
124
+ >
125
+ Sign in with Lumera
126
+ </button>
127
+ )}
112
128
  </div>
113
129
  </main>
114
130
  );
@@ -20,6 +20,8 @@ export default defineConfig({
20
20
  dedupe: ['react', 'react-dom'],
21
21
  },
22
22
  server: {
23
- allowedHosts: ['mac.lumerahq.com'],
23
+ // Local platform host plus standalone app hostnames
24
+ // (<project>--<company>.<env-suffix> on lumerahq.app).
25
+ allowedHosts: ['mac.lumerahq.com', '.lumerahq.app'],
24
26
  },
25
27
  });