@wix/himalaya-cli 0.1045.0 → 0.1047.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.
@@ -19,7 +19,7 @@ export interface ExperimentResult {
19
19
  variant: string;
20
20
  }
21
21
  export declare const experiments: {
22
- /** Conduct a Petri experiment; returns the resolved variant (or `fallback`). */
22
+ /** The app's served variant for `spec` (`himi flags`), or `fallback` when none is assigned. */
23
23
  conduct(spec: string, fallback?: string): Promise<ExperimentResult>;
24
24
  /** Boolean convenience — true when the variant is exactly "true". */
25
25
  isEnabled(spec: string): Promise<boolean>;
@@ -32,7 +32,7 @@ export declare const experiments: {
32
32
  }>;
33
33
  };
34
34
  export declare const flags: {
35
- /** Whether a Petri feature-toggle spec is ON. */
35
+ /** Whether the app's served flag `spec` is ON (variant `on`/`true`/`new`), else `fallback`. */
36
36
  isEnabled(spec: string, fallback?: boolean): Promise<boolean>;
37
37
  /** A toggle's raw value (variant string) for non-boolean specs. */
38
38
  value(spec: string, fallback?: string): Promise<string>;
@@ -62,40 +62,15 @@ function flights() {
62
62
  function dropConductFlights() {
63
63
  flights()?.inflight.clear();
64
64
  }
65
- var conductedReal = (value) => ({ value, cacheable: true });
66
- var conductedSeam = (value) => ({ value, cacheable: false });
67
- async function l2Get(ns, spec) {
68
- try {
69
- const r = await call("conductedGet", { door: ns, spec });
70
- return {
71
- value: r?.hit && typeof r.value === "string" && r.value.length > 0 ? r.value : null,
72
- scope: typeof r?.scope === "string" && r.scope.length > 0 ? r.scope : null
73
- };
74
- } catch {
75
- return { value: null, scope: null };
76
- }
77
- }
78
- async function l2Put(ns, spec, value, scope) {
79
- try {
80
- await call("conductedPut", { door: ns, spec, value, scope });
81
- } catch {
82
- }
83
- }
84
65
  async function conductCached(ns, spec, fallback, resolve) {
85
66
  const f = flights();
86
- if (!f) return (await resolve()).value;
67
+ if (!f) return resolve();
87
68
  const flightKey = JSON.stringify([ns, spec, fallback]);
88
69
  const pending = f.inflight.get(flightKey);
89
70
  if (pending) return pending;
90
71
  const task = (async () => {
91
72
  try {
92
- const { value: cached, scope } = await l2Get(ns, spec);
93
- if (cached !== null) return cached;
94
- const { value, cacheable } = await resolve();
95
- if (cacheable && value !== null && value.length > 0 && scope !== null) {
96
- await l2Put(ns, spec, value, scope);
97
- }
98
- return value;
73
+ return await resolve();
99
74
  } finally {
100
75
  f.inflight.delete(flightKey);
101
76
  }
@@ -103,31 +78,16 @@ async function conductCached(ns, spec, fallback, resolve) {
103
78
  f.inflight.set(flightKey, task);
104
79
  return task;
105
80
  }
106
- var PETRI_CONDUCT = "/_api/wix-petri-webapp/v1/conductExperiment";
107
81
  async function resolveExperiment(spec, fallback) {
108
- if (isMemberReal("experiments")) {
109
- try {
110
- const r2 = await realWix("POST", PETRI_CONDUCT, { key: spec, fallbackValue: fallback });
111
- if (typeof r2?.value === "string") return conductedReal(r2.value);
112
- } catch {
113
- }
114
- }
115
82
  const r = await call("experimentsConduct", { spec, fallback });
116
- return conductedSeam(typeof r?.variant === "string" ? r.variant : null);
83
+ return typeof r?.variant === "string" ? r.variant : null;
117
84
  }
118
85
  async function resolveFlag(spec, fallback) {
119
- if (isMemberReal("flags")) {
120
- try {
121
- const r2 = await realWix("POST", PETRI_CONDUCT, { key: spec, fallbackValue: String(fallback) });
122
- if (typeof r2?.value === "string") return conductedReal(r2.value);
123
- } catch {
124
- }
125
- }
126
86
  const r = await call("flagsIsEnabled", { spec, fallback });
127
- return conductedSeam(typeof r?.enabled === "boolean" ? String(r.enabled) : null);
87
+ return typeof r?.enabled === "boolean" ? String(r.enabled) : null;
128
88
  }
129
89
  var experiments = {
130
- /** Conduct a Petri experiment; returns the resolved variant (or `fallback`). */
90
+ /** The app's served variant for `spec` (`himi flags`), or `fallback` when none is assigned. */
131
91
  async conduct(spec, fallback = "false") {
132
92
  const resolved = await conductCached("experiments", spec, fallback, () => resolveExperiment(spec, fallback));
133
93
  return { spec, variant: resolved ?? fallback };
@@ -146,7 +106,7 @@ var experiments = {
146
106
  }
147
107
  };
148
108
  var flags = {
149
- /** Whether a Petri feature-toggle spec is ON. */
109
+ /** Whether the app's served flag `spec` is ON (variant `on`/`true`/`new`), else `fallback`. */
150
110
  async isEnabled(spec, fallback = false) {
151
111
  const resolved = await conductCached("flags", spec, String(fallback), () => resolveFlag(spec, fallback));
152
112
  if (resolved === null) return fallback;
@@ -186,25 +146,11 @@ var i18n = {
186
146
  var ping = {
187
147
  /** Register the device's push token with Wix Ping. */
188
148
  async register() {
189
- if (isMemberReal("ping")) {
190
- try {
191
- const r2 = await realWix("POST", "/_api/ping-webapp/v1/registrations", {});
192
- if (r2 && typeof r2.registered === "boolean") return { registered: r2.registered, token: r2.token ?? "" };
193
- } catch {
194
- }
195
- }
196
149
  const r = await call("pingRegister");
197
150
  return { registered: r?.registered ?? false, token: r?.token ?? "" };
198
151
  },
199
152
  /** Current Ping registration status (no network). */
200
153
  async status() {
201
- if (isMemberReal("ping")) {
202
- try {
203
- const r2 = await realWix("GET", "/_api/ping-webapp/v1/registrations");
204
- if (r2 && typeof r2.registered === "boolean") return { registered: r2.registered, token: r2.token ?? "" };
205
- } catch {
206
- }
207
- }
208
154
  const r = await call("pingStatus");
209
155
  return { registered: r?.registered ?? false, token: r?.token ?? "" };
210
156
  }
@@ -212,13 +158,6 @@ var ping = {
212
158
  var deeplink = {
213
159
  /** Resolve a deferred (post-install) deep link via Wix Jot. Null when none. */
214
160
  async resolveDeferred() {
215
- if (isMemberReal("deeplink")) {
216
- try {
217
- const r2 = await realWix("GET", "/_api/app-link-server/v1/deferred-deep-link");
218
- if (r2) return { link: r2.link && r2.link.length > 0 ? r2.link : null };
219
- } catch {
220
- }
221
- }
222
161
  const r = await call("deeplinkResolveDeferred");
223
162
  return { link: r?.link && r.link.length > 0 ? r.link : null };
224
163
  }
package/dist/cli.mjs CHANGED
@@ -18830,6 +18830,204 @@ ${cookieHeader}`);
18830
18830
  }
18831
18831
  });
18832
18832
 
18833
+ // ../../core/ts/src/flags-engine.ts
18834
+ function bucket(key2) {
18835
+ const digest2 = sha1(key2);
18836
+ const n = (digest2[0] << 24 | digest2[1] << 16 | digest2[2] << 8 | digest2[3]) >>> 0;
18837
+ return n % 1e4;
18838
+ }
18839
+ function attrValue(ctx, attribute) {
18840
+ switch (attribute) {
18841
+ case "platform":
18842
+ return ctx.platform;
18843
+ case "appVersion":
18844
+ return ctx.appVersion;
18845
+ case "locale":
18846
+ return ctx.locale;
18847
+ case "appId":
18848
+ return ctx.appId;
18849
+ case "userId":
18850
+ return ctx.userId;
18851
+ default:
18852
+ return ctx.custom?.[attribute];
18853
+ }
18854
+ }
18855
+ function clauseMatches(ctx, c) {
18856
+ const v = attrValue(ctx, c.attribute);
18857
+ switch (c.op) {
18858
+ case "eq":
18859
+ return v !== void 0 && c.values.includes(v);
18860
+ case "neq":
18861
+ return v === void 0 || !c.values.includes(v);
18862
+ case "in":
18863
+ return v !== void 0 && c.values.includes(v);
18864
+ case "contains":
18865
+ return v !== void 0 && c.values.some((needle) => v.includes(needle));
18866
+ default:
18867
+ return false;
18868
+ }
18869
+ }
18870
+ function ruleMatches(ctx, rule) {
18871
+ return rule.clauses.length > 0 && rule.clauses.every((c) => clauseMatches(ctx, c));
18872
+ }
18873
+ function variantForBucket(def, b) {
18874
+ const total = def.variants.reduce((s, v) => s + Math.max(0, v.weight), 0);
18875
+ if (total <= 0) return def.defaultVariant;
18876
+ const target = b / 1e4 * total;
18877
+ let acc = 0;
18878
+ for (const v of def.variants) {
18879
+ acc += Math.max(0, v.weight);
18880
+ if (target < acc) return v.key;
18881
+ }
18882
+ return def.variants[def.variants.length - 1].key;
18883
+ }
18884
+ function evaluate(def, ctx, override) {
18885
+ const base = { key: def.key, experimentId: def.experiment?.id };
18886
+ if (override) return { ...base, variant: override, reason: "override" };
18887
+ if (!def.enabled) return { ...base, variant: def.defaultVariant, reason: "disabled" };
18888
+ for (const rule of def.targeting ?? []) {
18889
+ if (ruleMatches(ctx, rule)) return { ...base, variant: rule.variant, reason: "targeting" };
18890
+ }
18891
+ const id = ctx.userId ?? ctx.sessionId;
18892
+ const seed2 = def.seed ?? def.key;
18893
+ const rollout = def.rolloutPercentage ?? 100;
18894
+ if (rollout < 100) {
18895
+ const gate = bucket(`${id}:${seed2}:rollout`);
18896
+ if (gate >= rollout * 100) return { ...base, variant: def.defaultVariant, reason: "default" };
18897
+ }
18898
+ const variant = variantForBucket(def, bucket(`${id}:${seed2}`));
18899
+ return { ...base, variant, reason: "rollout" };
18900
+ }
18901
+ function evaluateAll(defs, ctx, overrides) {
18902
+ const out = {};
18903
+ for (const def of defs) out[def.key] = evaluate(def, ctx, overrides?.[def.key]);
18904
+ return out;
18905
+ }
18906
+ function utf8(input) {
18907
+ return new TextEncoder().encode(input);
18908
+ }
18909
+ function sha1(input) {
18910
+ const bytes2 = typeof input === "string" ? utf8(input) : input;
18911
+ const bitLen = bytes2.length * 8;
18912
+ const padded = new Uint8Array(((bytes2.length + 9 + 63) / 64 | 0) * 64);
18913
+ padded.set(bytes2);
18914
+ padded[bytes2.length] = 128;
18915
+ const view = new DataView(padded.buffer);
18916
+ view.setUint32(padded.length - 8, Math.floor(bitLen / 4294967296));
18917
+ view.setUint32(padded.length - 4, bitLen >>> 0);
18918
+ let h0 = 1732584193, h1 = 4023233417, h2 = 2562383102, h3 = 271733878, h4 = 3285377520;
18919
+ const w = new Uint32Array(80);
18920
+ for (let off = 0; off < padded.length; off += 64) {
18921
+ for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4);
18922
+ for (let i = 16; i < 80; i++) {
18923
+ const v = w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16];
18924
+ w[i] = (v << 1 | v >>> 31) >>> 0;
18925
+ }
18926
+ let a = h0, b = h1, c = h2, d = h3, e = h4;
18927
+ for (let i = 0; i < 80; i++) {
18928
+ let f;
18929
+ let k;
18930
+ if (i < 20) {
18931
+ f = b & c | ~b & d;
18932
+ k = 1518500249;
18933
+ } else if (i < 40) {
18934
+ f = b ^ c ^ d;
18935
+ k = 1859775393;
18936
+ } else if (i < 60) {
18937
+ f = b & c | b & d | c & d;
18938
+ k = 2400959708;
18939
+ } else {
18940
+ f = b ^ c ^ d;
18941
+ k = 3395469782;
18942
+ }
18943
+ const t = ((a << 5 | a >>> 27) >>> 0) + (f >>> 0) + e + k + w[i] >>> 0;
18944
+ e = d;
18945
+ d = c;
18946
+ c = (b << 30 | b >>> 2) >>> 0;
18947
+ b = a;
18948
+ a = t;
18949
+ }
18950
+ h0 = h0 + a >>> 0;
18951
+ h1 = h1 + b >>> 0;
18952
+ h2 = h2 + c >>> 0;
18953
+ h3 = h3 + d >>> 0;
18954
+ h4 = h4 + e >>> 0;
18955
+ }
18956
+ const out = new Uint8Array(20);
18957
+ const outView = new DataView(out.buffer);
18958
+ outView.setUint32(0, h0);
18959
+ outView.setUint32(4, h1);
18960
+ outView.setUint32(8, h2);
18961
+ outView.setUint32(12, h3);
18962
+ outView.setUint32(16, h4);
18963
+ return out;
18964
+ }
18965
+ var init_flags_engine = __esm({
18966
+ "../../core/ts/src/flags-engine.ts"() {
18967
+ }
18968
+ });
18969
+
18970
+ // ../../core/serve-runtime/src/flag-assignments.ts
18971
+ function bad(error) {
18972
+ return new Response(JSON.stringify({ error }), { status: 400, headers: { "content-type": JSON_CT, "cache-control": "no-store" } });
18973
+ }
18974
+ function flagAssignmentsBody(app, defs, ctx, overrides, ttlSeconds = FLAG_ASSIGNMENTS_TTL_SECONDS) {
18975
+ const evalCtx = {
18976
+ sessionId: ctx.id,
18977
+ appId: app,
18978
+ ...ctx.platform ? { platform: ctx.platform } : {},
18979
+ ...ctx.appVersion ? { appVersion: ctx.appVersion } : {},
18980
+ ...ctx.locale ? { locale: ctx.locale } : {}
18981
+ };
18982
+ const byKey = new Map(defs.map((d) => [d.key, d]));
18983
+ const assignments = {};
18984
+ for (const [key2, a] of Object.entries(evaluateAll(defs, evalCtx, overrides))) {
18985
+ const value = byKey.get(key2)?.variants.find((v) => v.key === a.variant)?.value;
18986
+ assignments[key2] = {
18987
+ variant: a.variant,
18988
+ reason: a.reason,
18989
+ ...a.experimentId ? { experimentId: a.experimentId } : {},
18990
+ ...value !== void 0 ? { value } : {}
18991
+ };
18992
+ }
18993
+ return { app, id: ctx.id, assignments, ttlSeconds };
18994
+ }
18995
+ function flagAssignmentsResponse(request, app, defs, overrides, ttlSeconds = FLAG_ASSIGNMENTS_TTL_SECONDS) {
18996
+ if (request.method !== "GET" && request.method !== "HEAD") {
18997
+ return new Response(JSON.stringify({ error: "method_not_allowed" }), { status: 405, headers: { "content-type": JSON_CT } });
18998
+ }
18999
+ const url = new URL(request.url);
19000
+ const id = url.searchParams.get("id") ?? "";
19001
+ if (!id) return bad("missing_id");
19002
+ if (id.length > MAX_ID_LENGTH) return bad("id_too_long");
19003
+ const q = (k) => url.searchParams.get(k) || void 0;
19004
+ const body = JSON.stringify(flagAssignmentsBody(app, defs.filter(isEvaluable), {
19005
+ id,
19006
+ platform: q("platform"),
19007
+ appVersion: q("appVersion"),
19008
+ locale: q("locale")
19009
+ }, overrides, ttlSeconds));
19010
+ const etag = strongEtag(body);
19011
+ const headers2 = { "content-type": JSON_CT, etag, "cache-control": `private, max-age=${Math.min(ttlSeconds, 60)}` };
19012
+ if (request.headers.get("if-none-match") === etag) return new Response(null, { status: 304, headers: headers2 });
19013
+ return new Response(request.method === "HEAD" ? null : body, { status: 200, headers: headers2 });
19014
+ }
19015
+ function isEvaluable(d) {
19016
+ const x = d;
19017
+ return !!x && typeof x.key === "string" && Array.isArray(x.variants) && typeof x.defaultVariant === "string";
19018
+ }
19019
+ var FLAG_ASSIGNMENTS_PATH, FLAG_ASSIGNMENTS_TTL_SECONDS, MAX_ID_LENGTH, JSON_CT;
19020
+ var init_flag_assignments = __esm({
19021
+ "../../core/serve-runtime/src/flag-assignments.ts"() {
19022
+ init_flags_engine();
19023
+ init_contract();
19024
+ FLAG_ASSIGNMENTS_PATH = /^\/v1\/flags\/([^/]+)\/assignments$/;
19025
+ FLAG_ASSIGNMENTS_TTL_SECONDS = 300;
19026
+ MAX_ID_LENGTH = 256;
19027
+ JSON_CT = "application/json; charset=utf-8";
19028
+ }
19029
+ });
19030
+
18833
19031
  // ../../core/dev-server/src/images.ts
18834
19032
  import { createHash as createHash11 } from "node:crypto";
18835
19033
  function svgFor(productId, title, sku) {
@@ -32031,143 +32229,6 @@ var init_flags = __esm({
32031
32229
  }
32032
32230
  });
32033
32231
 
32034
- // ../../core/ts/src/flags-engine.ts
32035
- function bucket(key2) {
32036
- const digest2 = sha1(key2);
32037
- const n = (digest2[0] << 24 | digest2[1] << 16 | digest2[2] << 8 | digest2[3]) >>> 0;
32038
- return n % 1e4;
32039
- }
32040
- function attrValue(ctx, attribute) {
32041
- switch (attribute) {
32042
- case "platform":
32043
- return ctx.platform;
32044
- case "appVersion":
32045
- return ctx.appVersion;
32046
- case "locale":
32047
- return ctx.locale;
32048
- case "appId":
32049
- return ctx.appId;
32050
- case "userId":
32051
- return ctx.userId;
32052
- default:
32053
- return ctx.custom?.[attribute];
32054
- }
32055
- }
32056
- function clauseMatches(ctx, c) {
32057
- const v = attrValue(ctx, c.attribute);
32058
- switch (c.op) {
32059
- case "eq":
32060
- return v !== void 0 && c.values.includes(v);
32061
- case "neq":
32062
- return v === void 0 || !c.values.includes(v);
32063
- case "in":
32064
- return v !== void 0 && c.values.includes(v);
32065
- case "contains":
32066
- return v !== void 0 && c.values.some((needle) => v.includes(needle));
32067
- default:
32068
- return false;
32069
- }
32070
- }
32071
- function ruleMatches(ctx, rule) {
32072
- return rule.clauses.length > 0 && rule.clauses.every((c) => clauseMatches(ctx, c));
32073
- }
32074
- function variantForBucket(def, b) {
32075
- const total = def.variants.reduce((s, v) => s + Math.max(0, v.weight), 0);
32076
- if (total <= 0) return def.defaultVariant;
32077
- const target = b / 1e4 * total;
32078
- let acc = 0;
32079
- for (const v of def.variants) {
32080
- acc += Math.max(0, v.weight);
32081
- if (target < acc) return v.key;
32082
- }
32083
- return def.variants[def.variants.length - 1].key;
32084
- }
32085
- function evaluate(def, ctx, override) {
32086
- const base = { key: def.key, experimentId: def.experiment?.id };
32087
- if (override) return { ...base, variant: override, reason: "override" };
32088
- if (!def.enabled) return { ...base, variant: def.defaultVariant, reason: "disabled" };
32089
- for (const rule of def.targeting ?? []) {
32090
- if (ruleMatches(ctx, rule)) return { ...base, variant: rule.variant, reason: "targeting" };
32091
- }
32092
- const id = ctx.userId ?? ctx.sessionId;
32093
- const seed2 = def.seed ?? def.key;
32094
- const rollout = def.rolloutPercentage ?? 100;
32095
- if (rollout < 100) {
32096
- const gate = bucket(`${id}:${seed2}:rollout`);
32097
- if (gate >= rollout * 100) return { ...base, variant: def.defaultVariant, reason: "default" };
32098
- }
32099
- const variant = variantForBucket(def, bucket(`${id}:${seed2}`));
32100
- return { ...base, variant, reason: "rollout" };
32101
- }
32102
- function evaluateAll(defs, ctx, overrides) {
32103
- const out = {};
32104
- for (const def of defs) out[def.key] = evaluate(def, ctx, overrides?.[def.key]);
32105
- return out;
32106
- }
32107
- function utf8(input) {
32108
- return new TextEncoder().encode(input);
32109
- }
32110
- function sha1(input) {
32111
- const bytes2 = typeof input === "string" ? utf8(input) : input;
32112
- const bitLen = bytes2.length * 8;
32113
- const padded = new Uint8Array(((bytes2.length + 9 + 63) / 64 | 0) * 64);
32114
- padded.set(bytes2);
32115
- padded[bytes2.length] = 128;
32116
- const view = new DataView(padded.buffer);
32117
- view.setUint32(padded.length - 8, Math.floor(bitLen / 4294967296));
32118
- view.setUint32(padded.length - 4, bitLen >>> 0);
32119
- let h0 = 1732584193, h1 = 4023233417, h2 = 2562383102, h3 = 271733878, h4 = 3285377520;
32120
- const w = new Uint32Array(80);
32121
- for (let off = 0; off < padded.length; off += 64) {
32122
- for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4);
32123
- for (let i = 16; i < 80; i++) {
32124
- const v = w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16];
32125
- w[i] = (v << 1 | v >>> 31) >>> 0;
32126
- }
32127
- let a = h0, b = h1, c = h2, d = h3, e = h4;
32128
- for (let i = 0; i < 80; i++) {
32129
- let f;
32130
- let k;
32131
- if (i < 20) {
32132
- f = b & c | ~b & d;
32133
- k = 1518500249;
32134
- } else if (i < 40) {
32135
- f = b ^ c ^ d;
32136
- k = 1859775393;
32137
- } else if (i < 60) {
32138
- f = b & c | b & d | c & d;
32139
- k = 2400959708;
32140
- } else {
32141
- f = b ^ c ^ d;
32142
- k = 3395469782;
32143
- }
32144
- const t = ((a << 5 | a >>> 27) >>> 0) + (f >>> 0) + e + k + w[i] >>> 0;
32145
- e = d;
32146
- d = c;
32147
- c = (b << 30 | b >>> 2) >>> 0;
32148
- b = a;
32149
- a = t;
32150
- }
32151
- h0 = h0 + a >>> 0;
32152
- h1 = h1 + b >>> 0;
32153
- h2 = h2 + c >>> 0;
32154
- h3 = h3 + d >>> 0;
32155
- h4 = h4 + e >>> 0;
32156
- }
32157
- const out = new Uint8Array(20);
32158
- const outView = new DataView(out.buffer);
32159
- outView.setUint32(0, h0);
32160
- outView.setUint32(4, h1);
32161
- outView.setUint32(8, h2);
32162
- outView.setUint32(12, h3);
32163
- outView.setUint32(16, h4);
32164
- return out;
32165
- }
32166
- var init_flags_engine = __esm({
32167
- "../../core/ts/src/flags-engine.ts"() {
32168
- }
32169
- });
32170
-
32171
32232
  // ../../core/ts/src/launch-manifest.ts
32172
32233
  import Ajv2 from "ajv";
32173
32234
  var directiveObject, LAUNCH_MANIFEST_SCHEMA;
@@ -36233,6 +36294,23 @@ function createDevServer(opts = {}) {
36233
36294
  const k = readKit(kitM[1]);
36234
36295
  return k ? sendJson(res, 200, k, { "access-control-allow-origin": "*" }) : sendJson(res, 404, { error: "unknown_kit", id: kitM[1] }, { "access-control-allow-origin": "*" });
36235
36296
  }
36297
+ const flagAssignM = /^\/v1\/flags\/([^/?]+)\/assignments(?:\?|$)/.exec(url);
36298
+ if (flagAssignM && (req.method === "GET" || req.method === "HEAD")) {
36299
+ await state.flagSync.refresh(state.app?.dir, state.flagStore, Date.now());
36300
+ const ours = flagAssignM[1] === state.appName;
36301
+ const answer = flagAssignmentsResponse(
36302
+ new Request(`http://dev-server${url}`, {
36303
+ method: req.method,
36304
+ headers: typeof req.headers["if-none-match"] === "string" ? { "if-none-match": req.headers["if-none-match"] } : {}
36305
+ }),
36306
+ flagAssignM[1],
36307
+ ours ? state.flagStore.list() : [],
36308
+ ours ? currentFlagConfig(state)?.overrides : void 0
36309
+ );
36310
+ res.writeHead(answer.status, { ...Object.fromEntries(answer.headers), "access-control-allow-origin": "*" });
36311
+ res.end(answer.status === 200 && req.method === "GET" ? await answer.text() : void 0);
36312
+ return;
36313
+ }
36236
36314
  if (url.startsWith("/v1/revocations") && req.method === "GET") {
36237
36315
  let revoked = [];
36238
36316
  if (state.app) {
@@ -36455,6 +36533,7 @@ var init_src6 = __esm({
36455
36533
  init_wix_gateway();
36456
36534
  init_wix_member_login();
36457
36535
  init_wix_bridge();
36536
+ init_flag_assignments();
36458
36537
  init_images();
36459
36538
  init_l10n();
36460
36539
  init_l10n_catalog();
@@ -42759,7 +42838,7 @@ function serializeAt(node, src, open) {
42759
42838
  const indent = lineIndent(src, open);
42760
42839
  return JSON.stringify(node, null, 2).split("\n").map((line, i) => i === 0 ? line : indent + line).join("\n");
42761
42840
  }
42762
- function patchGrids(src, installed, patch) {
42841
+ function patchGrids(src, installed, patch, keepFaq = false, dropFaq = false) {
42763
42842
  const OPEN = /"id": "([A-Za-z_0-9]+(?:\.[A-Za-z_0-9]+)*\.verticals)"/g;
42764
42843
  let out = "";
42765
42844
  let cursor = 0;
@@ -42780,12 +42859,17 @@ function patchGrids(src, installed, patch) {
42780
42859
  const prefix = m[1].slice(0, -".verticals".length);
42781
42860
  const baked = /* @__PURE__ */ new Map();
42782
42861
  const foreignCells = [];
42783
- for (const item of items) {
42784
- const id = item.id;
42785
- const legacy = item.modifiers?.accessibilityLabel;
42862
+ let faqCell;
42863
+ const claimOf = (item) => {
42864
+ const id = item?.id;
42865
+ const legacy = item?.modifiers?.accessibilityLabel;
42786
42866
  const claim = typeof id === "string" && id ? id : typeof legacy === "string" ? legacy : "";
42787
- const vertical = claim.startsWith(`${prefix}.`) ? claim.slice(prefix.length + 1) : "";
42867
+ return claim.startsWith(`${prefix}.`) ? claim.slice(prefix.length + 1) : "";
42868
+ };
42869
+ for (const item of items) {
42870
+ const vertical = claimOf(item);
42788
42871
  if (isKnown(vertical)) baked.set(vertical, item);
42872
+ else if (vertical === "faq" && keepFaq) faqCell = item;
42789
42873
  else if (!GRID_VERTICAL_LABELS.has(vertical)) foreignCells.push(item);
42790
42874
  }
42791
42875
  const wanted = [];
@@ -42803,10 +42887,36 @@ function patchGrids(src, installed, patch) {
42803
42887
  wanted.push(vertical);
42804
42888
  }
42805
42889
  const shape = baked.values().next().value ?? items[0];
42890
+ const bakedFaq = faqCell;
42891
+ if (keepFaq && !faqCell && hasScreen(src, FAQ_CARD.screenId)) {
42892
+ faqCell = cloneCell(shape, prefix, "faq", FAQ_CARD);
42893
+ }
42894
+ if (!installed.size) {
42895
+ if (dropFaq) {
42896
+ const kept = items.filter((item) => claimOf(item) !== "faq");
42897
+ if (kept.length !== items.length && kept.length > 0) {
42898
+ node.body.props.items = kept;
42899
+ out += src.slice(cursor, open) + serializeAt(node, src, open);
42900
+ cursor = close;
42901
+ OPEN.lastIndex = close;
42902
+ }
42903
+ continue;
42904
+ }
42905
+ const lastIsVertical = GRID_VERTICAL_LABELS.has(claimOf(items[items.length - 1]));
42906
+ if (!bakedFaq && faqCell && (items.length < EXPLORE_MAX_CELLS || lastIsVertical)) {
42907
+ if (items.length >= EXPLORE_MAX_CELLS) items.pop();
42908
+ items.push(faqCell);
42909
+ out += src.slice(cursor, open) + serializeAt(node, src, open);
42910
+ cursor = close;
42911
+ OPEN.lastIndex = close;
42912
+ }
42913
+ continue;
42914
+ }
42915
+ const cap = faqCell ? EXPLORE_MAX_CELLS - 1 : EXPLORE_MAX_CELLS;
42806
42916
  const next = [...foreignCells];
42807
42917
  const cells = [];
42808
42918
  for (const vertical of wanted) {
42809
- if (next.length >= EXPLORE_MAX_CELLS) break;
42919
+ if (next.length >= cap) break;
42810
42920
  const bakedCell = baked.get(vertical);
42811
42921
  if (bakedCell) {
42812
42922
  next.push(bakedCell);
@@ -42822,6 +42932,7 @@ function patchGrids(src, installed, patch) {
42822
42932
  cells.push(vertical);
42823
42933
  patch.added.push(vertical);
42824
42934
  }
42935
+ if (faqCell && next.length < EXPLORE_MAX_CELLS) next.push(faqCell);
42825
42936
  if (!cells.length) continue;
42826
42937
  patch.cells = cells;
42827
42938
  node.body.props.items = next;
@@ -42936,16 +43047,19 @@ function patchStoreCtas(src, verticals, homeActionId) {
42936
43047
  dropped
42937
43048
  };
42938
43049
  }
42939
- function patchHomeForVerticals(src, verticals, homeActionId) {
43050
+ function patchHomeForVerticals(src, verticals, homeActionId, opts = {}) {
42940
43051
  const installed = new Set(verticals.filter(isKnown));
42941
43052
  const patch = { src, cells: [], added: [], droppedModules: [], droppedCtas: [], unreachable: [] };
42942
- if (!installed.size) return patch;
42943
- const ctas = patchStoreCtas(patchModules(patchGrids(src, installed, patch), installed, patch), [...installed], homeActionId);
43053
+ if (!installed.size) {
43054
+ if (opts.keepFaq || opts.dropFaq) patch.src = patchGrids(src, installed, patch, !!opts.keepFaq, !opts.keepFaq && !!opts.dropFaq);
43055
+ return patch;
43056
+ }
43057
+ const ctas = patchStoreCtas(patchModules(patchGrids(src, installed, patch, opts.keepFaq), installed, patch), [...installed], homeActionId);
42944
43058
  patch.src = ctas.src;
42945
43059
  patch.droppedCtas = ctas.dropped;
42946
43060
  return patch;
42947
43061
  }
42948
- var EXPLORE_CARDS, EXPLORE_ORDER, EXPLORE_MAX_CELLS, MODULE_SOURCE_VERTICALS, isKnown, GRID_VERTICAL_LABELS, escapeRe2, textProps, STORE_SCREENS, isStoreScreen, screenDeclarations, isNode2;
43062
+ var EXPLORE_CARDS, EXPLORE_ORDER, EXPLORE_MAX_CELLS, MODULE_SOURCE_VERTICALS, isKnown, GRID_VERTICAL_LABELS, FAQ_CARD, escapeRe2, textProps, STORE_SCREENS, isStoreScreen, screenDeclarations, isNode2;
42949
43063
  var init_home_descriptor = __esm({
42950
43064
  "src/site-analyze/home-descriptor.ts"() {
42951
43065
  init_types2();
@@ -43017,6 +43131,7 @@ var init_home_descriptor = __esm({
43017
43131
  };
43018
43132
  isKnown = (v) => KNOWN_VERTICALS.includes(v);
43019
43133
  GRID_VERTICAL_LABELS = /* @__PURE__ */ new Set([...KNOWN_VERTICALS, "faq"]);
43134
+ FAQ_CARD = { icon: "help", screenId: "himi_faq", title: "FAQ", body: "Find answers to common questions." };
43020
43135
  escapeRe2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
43021
43136
  textProps = (n) => n?.body?.props;
43022
43137
  STORE_SCREENS = /* @__PURE__ */ new Set([
@@ -49467,7 +49582,7 @@ var init_wix_member_auth = __esm({
49467
49582
 
49468
49583
  // ../../core/serve/src/server.ts
49469
49584
  function json3(status, body, headers2 = {}) {
49470
- return new Response(JSON.stringify(body), { status, headers: { "content-type": JSON_CT, ...headers2 } });
49585
+ return new Response(JSON.stringify(body), { status, headers: { "content-type": JSON_CT2, ...headers2 } });
49471
49586
  }
49472
49587
  function isCookieAuthorizedPath(path2) {
49473
49588
  return path2 === "/v1/__webwix/call" || path2 === "/v1/owner" || path2.startsWith("/v1/owner/") || path2 === "/v1/__webwix/identity" || path2 === "/v1/__webwix/login" || path2 === "/v1/__webwix/register" || path2 === "/v1/__webwix/logout" || path2 === MEMBER_CALLBACK_PATH;
@@ -49627,7 +49742,7 @@ ${releaseId}`;
49627
49742
  if (request.headers.get("if-none-match") === etag) {
49628
49743
  return new Response(null, { status: 304, headers: { etag, "cache-control": cache, ...vary } });
49629
49744
  }
49630
- return new Response(body, { status: 200, headers: { "content-type": JSON_CT, etag, "cache-control": cache, ...vary } });
49745
+ return new Response(body, { status: 200, headers: { "content-type": JSON_CT2, etag, "cache-control": cache, ...vary } });
49631
49746
  }
49632
49747
  async function authorizePinnedRead(request, url, app, releaseId, screenId) {
49633
49748
  const denied = await checkPinnedRead({
@@ -49822,7 +49937,7 @@ ${releaseId}`;
49822
49937
  });
49823
49938
  }
49824
49939
  if ((request.method === "GET" || request.method === "HEAD") && path2 === "/v1/__webwix/identity") {
49825
- const headers2 = { "content-type": JSON_CT, "cache-control": "no-store" };
49940
+ const headers2 = { "content-type": JSON_CT2, "cache-control": "no-store" };
49826
49941
  if (request.method === "HEAD") return new Response(null, { status: 200, headers: headers2 });
49827
49942
  const webwixDirect = opts.webwixDirect ? await opts.webwixDirect() === true : opts.webwixBridge !== false;
49828
49943
  if (memberAuthOn()) {
@@ -50050,7 +50165,7 @@ ${releaseId}`;
50050
50165
  }
50051
50166
  return new Response(listBody, {
50052
50167
  status: 200,
50053
- headers: { "content-type": JSON_CT, ...listHeaders }
50168
+ headers: { "content-type": JSON_CT2, ...listHeaders }
50054
50169
  });
50055
50170
  }
50056
50171
  if (path2 === "/v1/templates") {
@@ -50107,7 +50222,7 @@ ${releaseId}`;
50107
50222
  }
50108
50223
  return new Response(bytes2, {
50109
50224
  status: 200,
50110
- headers: { "content-type": JSON_CT, etag, ...cache, "x-himalaya-app": app }
50225
+ headers: { "content-type": JSON_CT2, etag, ...cache, "x-himalaya-app": app }
50111
50226
  });
50112
50227
  }
50113
50228
  if (path2 === "/v1/authoring-docs" || /^\/v1\/authoring-docs\/(spec|mobile-ux)$/.test(path2)) {
@@ -50372,7 +50487,7 @@ ${releaseId}`;
50372
50487
  if (request.headers.get("if-none-match") === etag2) return new Response(null, { status: 304, headers: headers2 });
50373
50488
  const body2 = await pinnedBody(app, meta2.descriptorHash, () => store.getScreen(app, screenId));
50374
50489
  if (!body2) return json3(404, { error: "screen_not_found", screenId });
50375
- return new Response(body2, { status: 200, headers: { "content-type": JSON_CT, ...headers2, "x-himalaya-app": app } });
50490
+ return new Response(body2, { status: 200, headers: { "content-type": JSON_CT2, ...headers2, "x-himalaya-app": app } });
50376
50491
  }
50377
50492
  const m = await manifest(app);
50378
50493
  const meta = m?.screens[screenId];
@@ -50385,7 +50500,7 @@ ${releaseId}`;
50385
50500
  if (!body) return json3(404, { error: "screen_not_found", screenId });
50386
50501
  return new Response(body, {
50387
50502
  status: 200,
50388
- headers: { "content-type": JSON_CT, etag, "cache-control": DOC_CACHE_CONTROL, "x-himalaya-app": app }
50503
+ headers: { "content-type": JSON_CT2, etag, "cache-control": DOC_CACHE_CONTROL, "x-himalaya-app": app }
50389
50504
  });
50390
50505
  }
50391
50506
  if (path2 === "/v1/widgets") {
@@ -50413,7 +50528,7 @@ ${releaseId}`;
50413
50528
  if (!body) return json3(404, { error: "widget_not_found", widgetId });
50414
50529
  return new Response(body, {
50415
50530
  status: 200,
50416
- headers: { "content-type": JSON_CT, etag, "cache-control": cache, ...vary, "x-himalaya-app": app }
50531
+ headers: { "content-type": JSON_CT2, etag, "cache-control": cache, ...vary, "x-himalaya-app": app }
50417
50532
  });
50418
50533
  }
50419
50534
  return json3(404, { error: "not_found" });
@@ -50440,7 +50555,7 @@ ${releaseId}`;
50440
50555
  return withCors(request, response);
50441
50556
  };
50442
50557
  }
50443
- var JSON_CT;
50558
+ var JSON_CT2;
50444
50559
  var init_server = __esm({
50445
50560
  "../../core/serve/src/server.ts"() {
50446
50561
  init_sha256();
@@ -50451,7 +50566,7 @@ var init_server = __esm({
50451
50566
  init_package_share();
50452
50567
  init_wix_bridge();
50453
50568
  init_wix_member_auth();
50454
- JSON_CT = "application/json; charset=utf-8";
50569
+ JSON_CT2 = "application/json; charset=utf-8";
50455
50570
  }
50456
50571
  });
50457
50572
 
@@ -50632,6 +50747,8 @@ function makeRunHandler(opts) {
50632
50747
  { "cache-control": "max-age=30" }
50633
50748
  );
50634
50749
  }
50750
+ const flagAssign = FLAG_ASSIGNMENTS_PATH.exec(path2);
50751
+ if (flagAssign) return flagAssignmentsResponse(request, flagAssign[1], []);
50635
50752
  if (path2.startsWith("/v1/flags/")) {
50636
50753
  return json4(200, { app: opts.app, definitions: {} }, { "cache-control": "no-store" });
50637
50754
  }
@@ -50703,6 +50820,7 @@ var json4;
50703
50820
  var init_run_handler = __esm({
50704
50821
  "../../core/serve/src/run-handler.ts"() {
50705
50822
  init_server();
50823
+ init_flag_assignments();
50706
50824
  init_preview2();
50707
50825
  init_himi_package();
50708
50826
  json4 = (status, body, headers2 = {}) => new Response(JSON.stringify(body), {
@@ -50841,21 +50959,21 @@ var init_preview_assets_generated = __esm({
50841
50959
  "../../packages/serve/src/preview-assets.generated.ts"() {
50842
50960
  init_preview_assets_format();
50843
50961
  PREVIEW_ASSET_MANIFEST = [
50844
- { key: "assets/index-C8FaHVmV.js", contentType: "application/javascript; charset=utf-8", offset: 8, length: 1052014, sha256: "607f0eff78c1f286d1d94f6d6c08868e2deec8d39ace8176b374f0117026998a" },
50845
- { key: "fonts/Roboto-LICENSE.txt", contentType: "application/octet-stream", offset: 1052022, length: 4610, sha256: "994062dd7af4186a739de55dd9e151c63b6301f8e2cbafc608d35c2e87da0cd2" },
50846
- { key: "fonts/Roboto-Variable.ttf", contentType: "font/ttf", offset: 1056632, length: 488584, sha256: "d7598e12c5dbef095ff8272cfc55da0250bd07fbdecbac8a530b9b277872a134" },
50847
- { key: "fonts/WixMadeforDisplay-Bold.ttf", contentType: "font/ttf", offset: 1545216, length: 90924, sha256: "ec4c7d63772d980d6e147b2df6215fd2737e4b04cebe0cf7e11dc695fb63cdcf" },
50848
- { key: "fonts/WixMadeforDisplay-ExtraBold.ttf", contentType: "font/ttf", offset: 1636140, length: 90984, sha256: "bf4c16a2774edd38a921edf3099469866cd74c094fd596f3fa4e81f54a04dfaa" },
50849
- { key: "fonts/WixMadeforDisplay-Medium.ttf", contentType: "font/ttf", offset: 1727124, length: 90980, sha256: "368faca44fb9f7207e4af784e94750b1ee59c1098b8689e0f4a918926f244fb5" },
50850
- { key: "fonts/WixMadeforDisplay-Regular.ttf", contentType: "font/ttf", offset: 1818104, length: 90872, sha256: "97ed8f1a6772abe26e3d8af5cecfc4b02faa94497d585b0398b5053b739a70c1" },
50851
- { key: "fonts/WixMadeforDisplay-SemiBold.ttf", contentType: "font/ttf", offset: 1908976, length: 90980, sha256: "642feab8a48ddf6c7a89d7c2a7c0edfa0e6e773f671287e460e222703563191e" },
50852
- { key: "fonts/WixMadeforText-Bold.ttf", contentType: "font/ttf", offset: 1999956, length: 108636, sha256: "37c179b4fefb63ea9637322b84583a2df53aa226c150294eea1675c14b1b8e29" },
50853
- { key: "fonts/WixMadeforText-ExtraBold.ttf", contentType: "font/ttf", offset: 2108592, length: 109084, sha256: "62e2fe4718129f36ecdb0ae4de385bd6b81ecc876146447167c6c140fad699a9" },
50854
- { key: "fonts/WixMadeforText-Medium.ttf", contentType: "font/ttf", offset: 2217676, length: 109048, sha256: "4ecf270136af928612c76fe4ebade91d8156975e5ea4c0cf9225501dd1788e02" },
50855
- { key: "fonts/WixMadeforText-Regular.ttf", contentType: "font/ttf", offset: 2326724, length: 107700, sha256: "dee56f09fd3efc127c1ea95557edc74110c1460a46287da36e0af25efd341ef5" },
50856
- { key: "fonts/WixMadeforText-SemiBold.ttf", contentType: "font/ttf", offset: 2434424, length: 88108, sha256: "dced97a77624bc5f8b1ae65850360908297f4588e09feb6f22792d29dc7e765b" },
50857
- { key: "index.html", contentType: "text/html; charset=utf-8", offset: 2522532, length: 4947, sha256: "3fd26f396a5b472e603185fb99d5b905f37b98615b2bf95456e4e9b03ae05429" },
50858
- { key: "t5-frame.html", contentType: "text/html; charset=utf-8", offset: 2527479, length: 17209, sha256: "dffc9718e2faa63ae2cc919e0a842f585ed565aaaa7c53778e90379442c731b6" }
50962
+ { key: "assets/index-DT6bRpY_.js", contentType: "application/javascript; charset=utf-8", offset: 8, length: 1055972, sha256: "3a52c8395a6509cfe2cae8036024b987cd3361095ff5450bfd7a9efa0123850b" },
50963
+ { key: "fonts/Roboto-LICENSE.txt", contentType: "application/octet-stream", offset: 1055980, length: 4610, sha256: "994062dd7af4186a739de55dd9e151c63b6301f8e2cbafc608d35c2e87da0cd2" },
50964
+ { key: "fonts/Roboto-Variable.ttf", contentType: "font/ttf", offset: 1060590, length: 488584, sha256: "d7598e12c5dbef095ff8272cfc55da0250bd07fbdecbac8a530b9b277872a134" },
50965
+ { key: "fonts/WixMadeforDisplay-Bold.ttf", contentType: "font/ttf", offset: 1549174, length: 90924, sha256: "ec4c7d63772d980d6e147b2df6215fd2737e4b04cebe0cf7e11dc695fb63cdcf" },
50966
+ { key: "fonts/WixMadeforDisplay-ExtraBold.ttf", contentType: "font/ttf", offset: 1640098, length: 90984, sha256: "bf4c16a2774edd38a921edf3099469866cd74c094fd596f3fa4e81f54a04dfaa" },
50967
+ { key: "fonts/WixMadeforDisplay-Medium.ttf", contentType: "font/ttf", offset: 1731082, length: 90980, sha256: "368faca44fb9f7207e4af784e94750b1ee59c1098b8689e0f4a918926f244fb5" },
50968
+ { key: "fonts/WixMadeforDisplay-Regular.ttf", contentType: "font/ttf", offset: 1822062, length: 90872, sha256: "97ed8f1a6772abe26e3d8af5cecfc4b02faa94497d585b0398b5053b739a70c1" },
50969
+ { key: "fonts/WixMadeforDisplay-SemiBold.ttf", contentType: "font/ttf", offset: 1912934, length: 90980, sha256: "642feab8a48ddf6c7a89d7c2a7c0edfa0e6e773f671287e460e222703563191e" },
50970
+ { key: "fonts/WixMadeforText-Bold.ttf", contentType: "font/ttf", offset: 2003914, length: 108636, sha256: "37c179b4fefb63ea9637322b84583a2df53aa226c150294eea1675c14b1b8e29" },
50971
+ { key: "fonts/WixMadeforText-ExtraBold.ttf", contentType: "font/ttf", offset: 2112550, length: 109084, sha256: "62e2fe4718129f36ecdb0ae4de385bd6b81ecc876146447167c6c140fad699a9" },
50972
+ { key: "fonts/WixMadeforText-Medium.ttf", contentType: "font/ttf", offset: 2221634, length: 109048, sha256: "4ecf270136af928612c76fe4ebade91d8156975e5ea4c0cf9225501dd1788e02" },
50973
+ { key: "fonts/WixMadeforText-Regular.ttf", contentType: "font/ttf", offset: 2330682, length: 107700, sha256: "dee56f09fd3efc127c1ea95557edc74110c1460a46287da36e0af25efd341ef5" },
50974
+ { key: "fonts/WixMadeforText-SemiBold.ttf", contentType: "font/ttf", offset: 2438382, length: 88108, sha256: "dced97a77624bc5f8b1ae65850360908297f4588e09feb6f22792d29dc7e765b" },
50975
+ { key: "index.html", contentType: "text/html; charset=utf-8", offset: 2526490, length: 4947, sha256: "a9dcd2ec33b7ea5cee46b0a1f37f926678dd6939b442e3d50bff0101f0fe63ba" },
50976
+ { key: "t5-frame.html", contentType: "text/html; charset=utf-8", offset: 2531437, length: 17209, sha256: "dffc9718e2faa63ae2cc919e0a842f585ed565aaaa7c53778e90379442c731b6" }
50859
50977
  ];
50860
50978
  }
50861
50979
  });
@@ -54059,7 +54177,28 @@ function deriveTailoring(ctx, overrides = {}, revision = "site-derived") {
54059
54177
  };
54060
54178
  return { appName, iconSource: deriveIconSource(ctx), provisioning, missing };
54061
54179
  }
54062
- var clean, ID_MAX, NAME_MAX, TYPE_MAX, CONTROL_CHARS, HAS_CONTROL_CHAR, plain, shown;
54180
+ function bounded(s, max) {
54181
+ if (s.length <= max) return s;
54182
+ const cut = s.slice(0, max - 1);
54183
+ const space = cut.lastIndexOf(" ");
54184
+ return `${(space > max * 0.6 ? cut.slice(0, space) : cut).trimEnd()}\u2026`;
54185
+ }
54186
+ function deriveStaticFaq(faq) {
54187
+ if (!Array.isArray(faq)) return [];
54188
+ const out = [];
54189
+ const seen = /* @__PURE__ */ new Set();
54190
+ for (const raw of faq) {
54191
+ if (out.length >= STATIC_FAQ_LIMITS.pairs) break;
54192
+ const pair = raw && typeof raw === "object" ? raw : {};
54193
+ const q = bounded(oneLine(pair.q), STATIC_FAQ_LIMITS.question);
54194
+ const a = bounded(oneLine(pair.a), STATIC_FAQ_LIMITS.answer);
54195
+ if (!q || !a || seen.has(q.toLowerCase())) continue;
54196
+ seen.add(q.toLowerCase());
54197
+ out.push({ q, a });
54198
+ }
54199
+ return out;
54200
+ }
54201
+ var clean, ID_MAX, NAME_MAX, TYPE_MAX, CONTROL_CHARS, HAS_CONTROL_CHAR, plain, shown, STATIC_FAQ_LIMITS, oneLine;
54063
54202
  var init_derive = __esm({
54064
54203
  "src/site-analyze/derive.ts"() {
54065
54204
  init_types2();
@@ -54071,6 +54210,8 @@ var init_derive = __esm({
54071
54210
  HAS_CONTROL_CHAR = /[\u0000-\u001F\u007F]/;
54072
54211
  plain = (value, max) => (value ?? "").replace(CONTROL_CHARS, "").trim().slice(0, max);
54073
54212
  shown = (value) => (typeof value === "string" ? JSON.stringify(value) : String(value)).slice(0, 64);
54213
+ STATIC_FAQ_LIMITS = { pairs: 20, question: 200, answer: 1500 };
54214
+ oneLine = (s) => typeof s === "string" ? s.replace(/[\u0000-\u001F\u007F\s]+/g, " ").trim() : "";
54074
54215
  }
54075
54216
  });
54076
54217
 
@@ -57129,7 +57270,8 @@ function mergeCustomerOwned(committed, overlay) {
57129
57270
  "navigation",
57130
57271
  "locale",
57131
57272
  "wix",
57132
- "copy"
57273
+ "copy",
57274
+ "faq"
57133
57275
  ];
57134
57276
  const customer = {};
57135
57277
  for (const key2 of CUSTOMER_OWNED) {
@@ -57223,12 +57365,56 @@ function buildMergedContract(options) {
57223
57365
  staged.push(localPath);
57224
57366
  return { path: path2, staged };
57225
57367
  }
57368
+ function catalogTypography(overlay, template) {
57369
+ const typography = {};
57370
+ const notes = [];
57371
+ const base = template && typeof template === "object" ? template : {};
57372
+ const usable = (family) => typeof family === "string" && family.trim() !== "" && (family === SYSTEM_FONT_FAMILY || canonicalFamilyName(family) !== void 0);
57373
+ for (const [role, token] of Object.entries(overlay ?? {})) {
57374
+ const family = token?.family;
57375
+ if (typeof family !== "string" || family.trim() === "") {
57376
+ typography[role] = token;
57377
+ continue;
57378
+ }
57379
+ if (family === SYSTEM_FONT_FAMILY) {
57380
+ typography[role] = token;
57381
+ continue;
57382
+ }
57383
+ const canonical3 = canonicalFamilyName(family.trim());
57384
+ if (canonical3) {
57385
+ typography[role] = { ...token, family: canonical3 };
57386
+ continue;
57387
+ }
57388
+ const kept = base[role]?.family;
57389
+ const using = usable(kept) ? `the template's ${kept}` : `the template's default (${TIER1_FLOOR[0]})`;
57390
+ notes.push(`site font "${family}" (${role}) isn't in the font catalog; using ${using}`);
57391
+ }
57392
+ return { typography, notes };
57393
+ }
57394
+ function staticFaqFor(overlay, overlayPath) {
57395
+ const own3 = overlay.faq;
57396
+ if (own3 !== void 0) return deriveStaticFaq(own3);
57397
+ const bookPath = join59(dirname32(resolve45(overlayPath)), "book.json");
57398
+ if (!existsSync61(bookPath)) return void 0;
57399
+ try {
57400
+ const book = JSON.parse(readFileSync72(bookPath, "utf8"));
57401
+ const copy = book?.copy;
57402
+ if (!copy || typeof copy !== "object" || copy.unavailable) return void 0;
57403
+ const pairs = deriveStaticFaq(copy.faq);
57404
+ if (!pairs.length && copy.partial) return void 0;
57405
+ return pairs;
57406
+ } catch {
57407
+ return void 0;
57408
+ }
57409
+ }
57226
57410
  async function applyOverlays(input) {
57227
57411
  const cwd = resolve45(input.cwd);
57228
57412
  const shell = input.shell ?? DEFAULT_BRANDABLE_SHELL;
57229
57413
  assertKnownShell(shell);
57230
57414
  const overlay = JSON.parse(readFileSync72(input.overlayPath, "utf8"));
57231
57415
  const tokens = JSON.parse(readFileSync72(input.tokensPath, "utf8"));
57416
+ const faq = staticFaqFor(overlay, input.overlayPath);
57417
+ if (faq !== void 0) overlay.faq = faq;
57232
57418
  const wrote = [];
57233
57419
  const notes = [];
57234
57420
  const repoRoot2 = findRepoRoot(cwd);
@@ -57262,9 +57448,11 @@ async function applyOverlays(input) {
57262
57448
  }
57263
57449
  const pkgProv = join59(cwd, "provisioning.json");
57264
57450
  assertNotCommitted(pkgProv, input.forbiddenProvisioning);
57451
+ let packageFaq = faq;
57265
57452
  if (existsSync61(pkgProv)) {
57266
57453
  const cur = JSON.parse(readFileSync72(pkgProv, "utf8"));
57267
57454
  const merged = deepMerge4(cur, overlay);
57455
+ packageFaq = merged.faq;
57268
57456
  writeFileSync27(pkgProv, JSON.stringify(merged, null, 2) + "\n");
57269
57457
  wrote.push(pkgProv);
57270
57458
  } else {
@@ -57274,7 +57462,13 @@ async function applyOverlays(input) {
57274
57462
  const pkgTokens = join59(cwd, "tokens.json");
57275
57463
  if (existsSync61(pkgTokens) && (tokens.colors || tokens.typography)) {
57276
57464
  const cur = JSON.parse(readFileSync72(pkgTokens, "utf8"));
57277
- const merged = deepMerge4(cur, tokens);
57465
+ let tokensIn = tokens;
57466
+ if (tokens.typography) {
57467
+ const fonts = catalogTypography(tokens.typography, cur.typography);
57468
+ notes.push(...fonts.notes);
57469
+ tokensIn = { ...tokens, typography: fonts.typography };
57470
+ }
57471
+ const merged = deepMerge4(cur, tokensIn);
57278
57472
  const mergedColors = merged.colors;
57279
57473
  if (mergedColors && typeof mergedColors === "object" && !Array.isArray(mergedColors)) {
57280
57474
  merged.colors = alignAccentsToSurfaces(mergedColors, tokens.colors);
@@ -57297,8 +57491,16 @@ async function applyOverlays(input) {
57297
57491
  const prev = readFileSync72(homeSrc, "utf8");
57298
57492
  const patched = patchBrandParams(prev, overlay, homeAction);
57299
57493
  let next = patched.src;
57300
- if (!patched.foreign && overlay.installedVerticals?.length) {
57301
- const home = patchHomeForVerticals(next, overlay.installedVerticals, homeAction);
57494
+ if (!patched.foreign && faq?.length && prev.includes('"tier5.himiBrandedFaq.load"') && !prev.includes('"staticFaq"')) {
57495
+ notes.push(
57496
+ "the site's FAQ pairs were saved to provisioning.json, but this package's FAQ screen predates the site-FAQ fallback and will not show them \u2014 re-scaffold from the current template to pick it up"
57497
+ );
57498
+ }
57499
+ const keepFaq = Array.isArray(packageFaq) && packageFaq.length > 0;
57500
+ const dropFaq = Array.isArray(faq) && faq.length === 0;
57501
+ const ownsHome = prev.includes(`"actionId": "${homeAction}"`);
57502
+ if (ownsHome && !patched.foreign && (overlay.installedVerticals?.length || keepFaq || dropFaq)) {
57503
+ const home = patchHomeForVerticals(next, overlay.installedVerticals ?? [], homeAction, { keepFaq, dropFaq });
57302
57504
  next = home.src;
57303
57505
  if (home.droppedCtas.length) {
57304
57506
  notes.push(`removed the home's store call(s) to action (${home.droppedCtas.join(", ")}) \u2014 this site has no store`);
@@ -57344,10 +57546,18 @@ function patchBrandParams(src, overlay, homeActionId) {
57344
57546
  const layout = overlay.appearance?.homeLayout;
57345
57547
  const accent = overlay.branding?.primaryColor;
57346
57548
  const verts = overlay.installedVerticals;
57549
+ const faq = overlay.faq;
57347
57550
  const edits = [
57348
57551
  ...layout ? [{ pattern: /"ownerDefaultLayout": "[^"]*"/g, replacement: `"ownerDefaultLayout": ${JSON.stringify(layout)}` }] : [],
57349
57552
  ...accent ? [{ pattern: /"brandAccent": "[^"]*"/g, replacement: `"brandAccent": ${JSON.stringify(accent)}` }] : [],
57350
- ...verts?.length ? [{ pattern: /"verticals": "\[[^\]]*\]"/g, replacement: `"verticals": ${JSON.stringify(JSON.stringify(verts))}` }] : []
57553
+ ...verts?.length ? [{ pattern: /"verticals": "\[[^\]]*\]"/g, replacement: `"verticals": ${JSON.stringify(JSON.stringify(verts))}` }] : [],
57554
+ // The FAQ screen's fallback pairs (#3276), a JSON STRING like `verticals` because action
57555
+ // params are string-valued. The pattern matches any escaped string, so re-applying a
57556
+ // package whose pairs are already written replaces them instead of missing. A function
57557
+ // replacement, because the pairs are customer text and may contain `$&`. Patched whenever
57558
+ // the analysis SAID something, `[]` included, so a re-apply clears pairs an earlier one
57559
+ // baked in rather than leaving the worker another site's FAQ.
57560
+ ...faq ? [{ pattern: /"staticFaq": "(?:[^"\\]|\\.)*"/g, replacement: `"staticFaq": ${JSON.stringify(JSON.stringify(faq))}` }] : []
57351
57561
  ];
57352
57562
  if (!edits.length) return { src, applied: 0, unparsed: 0, foreign: false };
57353
57563
  if (!src.includes(`"actionId": "${homeActionId}"`)) {
@@ -57392,7 +57602,7 @@ function patchBrandParams(src, overlay, homeActionId) {
57392
57602
  pattern.lastIndex = 0;
57393
57603
  if (!pattern.test(block)) continue;
57394
57604
  pattern.lastIndex = 0;
57395
- block = block.replace(pattern, replacement);
57605
+ block = block.replace(pattern, () => replacement);
57396
57606
  applied += 1;
57397
57607
  }
57398
57608
  out += src.slice(cursor, open) + block;
@@ -57410,6 +57620,10 @@ var init_apply2 = __esm({
57410
57620
  init_branding_derive();
57411
57621
  init_brand_color_math();
57412
57622
  init_home_descriptor();
57623
+ init_derive();
57624
+ init_catalog();
57625
+ init_plan_fonts();
57626
+ init_tokens();
57413
57627
  STAGED = ".himi/brand";
57414
57628
  }
57415
57629
  });
Binary file
@@ -3463,13 +3463,13 @@
3463
3463
  "name": "experiments",
3464
3464
  "kind": "const",
3465
3465
  "jsdoc": "",
3466
- "decl": "export const experiments = {\n /** Conduct a Petri experiment; returns the resolved variant (or `fallback`). */\n async conduct(spec: string, fallback = \"false\"): Promise<ExperimentResult> {\n const resolved = await conductCached(\"experiments\", spec, fallback, () => resolveExperiment(spec, fallback));\n return { spec, variant: resolved ?? fallback };\n },\n /** Boolean convenience — true when the variant is exactly \"true\". */\n async isEnabled(spec: string): Promise<boolean> {\n return (await experiments.conduct(spec, \"false\")).variant === \"true\";\n },\n /** Report A/B exposure for `spec`/`variant` — emits the platform's\n * `experiment_exposed` telemetry so outcome events attribute to the variant.\n * (Symphony relies on server-side conduction; this is the generalized app-side\n * exposure hook.) */\n async exposure(spec: string, variant: string): Promise<{ ok: boolean }> {\n const r = await call<{ ok?: boolean }>(\"experimentsExposure\", { spec, variant });\n return { ok: r?.ok ?? false };\n },\n};"
3466
+ "decl": "export const experiments = {\n /** The app's served variant for `spec` (`himi flags`), or `fallback` when none is assigned. */\n async conduct(spec: string, fallback = \"false\"): Promise<ExperimentResult> {\n const resolved = await conductCached(\"experiments\", spec, fallback, () => resolveExperiment(spec, fallback));\n return { spec, variant: resolved ?? fallback };\n },\n /** Boolean convenience — true when the variant is exactly \"true\". */\n async isEnabled(spec: string): Promise<boolean> {\n return (await experiments.conduct(spec, \"false\")).variant === \"true\";\n },\n /** Report A/B exposure for `spec`/`variant` — emits the platform's\n * `experiment_exposed` telemetry so outcome events attribute to the variant.\n * (Symphony relies on server-side conduction; this is the generalized app-side\n * exposure hook.) */\n async exposure(spec: string, variant: string): Promise<{ ok: boolean }> {\n const r = await call<{ ok?: boolean }>(\"experimentsExposure\", { spec, variant });\n return { ok: r?.ok ?? false };\n },\n};"
3467
3467
  },
3468
3468
  {
3469
3469
  "name": "flags",
3470
3470
  "kind": "const",
3471
3471
  "jsdoc": "",
3472
- "decl": "export const flags = {\n /** Whether a Petri feature-toggle spec is ON. */\n async isEnabled(spec: string, fallback = false): Promise<boolean> {\n const resolved = await conductCached(\"flags\", spec, String(fallback), () => resolveFlag(spec, fallback));\n if (resolved === null) return fallback;\n return resolved === \"true\" || resolved === \"new\";\n },\n /** A toggle's raw value (variant string) for non-boolean specs. */\n async value(spec: string, fallback = \"\"): Promise<string> {\n const r = await call<{ value?: string }>(\"flagsValue\", { spec, fallback });\n return r?.value ?? fallback;\n },\n /** All known toggle states (dev/diagnostic). */\n async all(): Promise<Record<string, string>> {\n const r = await call<{ flags?: Record<string, string> }>(\"flagsAll\");\n return r?.flags && typeof r.flags === \"object\" ? r.flags : {};\n },\n};"
3472
+ "decl": "export const flags = {\n /** Whether the app's served flag `spec` is ON (variant `on`/`true`/`new`), else `fallback`. */\n async isEnabled(spec: string, fallback = false): Promise<boolean> {\n const resolved = await conductCached(\"flags\", spec, String(fallback), () => resolveFlag(spec, fallback));\n if (resolved === null) return fallback;\n return resolved === \"true\" || resolved === \"new\";\n },\n /** A toggle's raw value (variant string) for non-boolean specs. */\n async value(spec: string, fallback = \"\"): Promise<string> {\n const r = await call<{ value?: string }>(\"flagsValue\", { spec, fallback });\n return r?.value ?? fallback;\n },\n /** All known toggle states (dev/diagnostic). */\n async all(): Promise<Record<string, string>> {\n const r = await call<{ flags?: Record<string, string> }>(\"flagsAll\");\n return r?.flags && typeof r.flags === \"object\" ? r.flags : {};\n },\n};"
3473
3473
  },
3474
3474
  {
3475
3475
  "name": "i18n",
@@ -3487,13 +3487,13 @@
3487
3487
  "name": "ping",
3488
3488
  "kind": "const",
3489
3489
  "jsdoc": "",
3490
- "decl": "export const ping = {\n /** Register the device's push token with Wix Ping. */\n async register(): Promise<PingRegistration> {\n if (isMemberReal(\"ping\")) {\n try {\n const r = await realWix<{ registered?: boolean; token?: string }>(\"POST\", \"/_api/ping-webapp/v1/registrations\", {});\n if (r && typeof r.registered === \"boolean\") return { registered: r.registered, token: r.token ?? \"\" };\n } catch { /* fall through to the dev seam */ }\n }\n const r = await call<{ registered?: boolean; token?: string }>(\"pingRegister\");\n return { registered: r?.registered ?? false, token: r?.token ?? \"\" };\n },\n /** Current Ping registration status (no network). */\n async status(): Promise<PingRegistration> {\n if (isMemberReal(\"ping\")) {\n try {\n const r = await realWix<{ registered?: boolean; token?: string }>(\"GET\", \"/_api/ping-webapp/v1/registrations\");\n if (r && typeof r.registered === \"boolean\") return { registered: r.registered, token: r.token ?? \"\" };\n } catch { /* fall through to the dev seam */ }\n }\n const r = await call<{ registered?: boolean; token?: string }>(\"pingStatus\");\n return { registered: r?.registered ?? false, token: r?.token ?? \"\" };\n },\n};"
3490
+ "decl": "export const ping = {\n /** Register the device's push token with Wix Ping. */\n async register(): Promise<PingRegistration> {\n const r = await call<{ registered?: boolean; token?: string }>(\"pingRegister\");\n return { registered: r?.registered ?? false, token: r?.token ?? \"\" };\n },\n /** Current Ping registration status (no network). */\n async status(): Promise<PingRegistration> {\n const r = await call<{ registered?: boolean; token?: string }>(\"pingStatus\");\n return { registered: r?.registered ?? false, token: r?.token ?? \"\" };\n },\n};"
3491
3491
  },
3492
3492
  {
3493
3493
  "name": "deeplink",
3494
3494
  "kind": "const",
3495
3495
  "jsdoc": "",
3496
- "decl": "export const deeplink = {\n /** Resolve a deferred (post-install) deep link via Wix Jot. Null when none. */\n async resolveDeferred(): Promise<{ link: string | null }> {\n if (isMemberReal(\"deeplink\")) {\n try {\n const r = await realWix<{ link?: string }>(\"GET\", \"/_api/app-link-server/v1/deferred-deep-link\");\n if (r) return { link: r.link && r.link.length > 0 ? r.link : null };\n } catch { /* fall through to the dev seam */ }\n }\n const r = await call<{ link?: string }>(\"deeplinkResolveDeferred\");\n return { link: r?.link && r.link.length > 0 ? r.link : null };\n },\n};"
3496
+ "decl": "export const deeplink = {\n /** Resolve a deferred (post-install) deep link via Wix Jot. Null when none. */\n async resolveDeferred(): Promise<{ link: string | null }> {\n const r = await call<{ link?: string }>(\"deeplinkResolveDeferred\");\n return { link: r?.link && r.link.length > 0 ? r.link : null };\n },\n};"
3497
3497
  },
3498
3498
  {
3499
3499
  "name": "SignedInstanceStatus",
@@ -26,7 +26,7 @@ import { bi, experiments, flags, i18n, auth, monitoring } from "@himalaya/wix-pl
26
26
  await bi.pageView("home");
27
27
  await bi.click("cta_start", "home");
28
28
 
29
- // experiments (Petri) / flags
29
+ // experiments / flags — the app's served flags (`himi flags`), never Wix Petri
30
30
  const variant = await experiments.conduct("specs.ai", "false"); // → { spec, variant }
31
31
  if (await flags.isEnabled("specs.newCheckout")) { /* … */ }
32
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1045.0",
3
+ "version": "0.1047.0",
4
4
  "type": "module",
5
5
  "description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
6
6
  "keywords": [
@@ -65,5 +65,5 @@
65
65
  "artifactId": "himalaya-cli"
66
66
  }
67
67
  },
68
- "falconPackageHash": "0a575418cdf281d15de4e62af6654a6d47a91cf26467dad0a437484b"
68
+ "falconPackageHash": "9a5f5cd39f2e85af3f506141935613aebbda4942b8cb91683db7492f"
69
69
  }
@@ -58239,7 +58239,8 @@ const SCREENS = runtimeSetting({
58239
58239
  "onAppear": {
58240
58240
  "actionId": "tier5.himiBrandedFaq.load",
58241
58241
  "params": {
58242
- "otherCategory": "Other"
58242
+ "otherCategory": "Other",
58243
+ "staticFaq": "[]"
58243
58244
  }
58244
58245
  }
58245
58246
  }
@@ -58,6 +58,55 @@ interface FaqState {
58
58
 
59
59
  const PAGE_SIZE = 100;
60
60
 
61
+ /** Caps on the static pairs, matching `BRANDED_LITE_FAQ_LIMITS` in the app contract. The
62
+ * param is re-bounded here because a worker must not trust a descriptor it did not build. */
63
+ const STATIC_LIMITS = { pairs: 20, question: 200, answer: 1500 } as const;
64
+ const STATIC_CATEGORY: FaqCategory = { id: "himi-static-faq", title: "" };
65
+
66
+ /** The site's own FAQ pairs — questions it publishes as page sections rather than
67
+ * through the Wix FAQ app, carried in by `site analyze --apply` as the `staticFaq` onAppear
68
+ * param (a JSON string: action params are string-valued). They are the FALLBACK: shown only
69
+ * when the FAQ app yields no visible question, so an owner-edited Wix FAQ always wins. */
70
+ let staticQuestions: FaqQuestion[] = [];
71
+
72
+ function parseStaticFaq(raw: unknown): FaqQuestion[] {
73
+ let pairs: unknown = raw;
74
+ if (typeof raw === "string") {
75
+ try {
76
+ pairs = JSON.parse(raw);
77
+ } catch {
78
+ // A malformed param is a DESCRIPTOR bug, not an empty FAQ: there is no fallback to
79
+ // offer, so the screen shows exactly what the Wix FAQ read says — its content, its
80
+ // empty card, or its error card — rather than pretending the site has no questions.
81
+ pairs = undefined;
82
+ }
83
+ }
84
+ if (!Array.isArray(pairs)) return [];
85
+ const out: FaqQuestion[] = [];
86
+ for (const pair of pairs) {
87
+ if (out.length >= STATIC_LIMITS.pairs) break;
88
+ const row = pair && typeof pair === "object" ? pair as { q?: unknown; a?: unknown } : {};
89
+ const question = typeof row.q === "string" ? row.q.trim().slice(0, STATIC_LIMITS.question) : "";
90
+ const answer = typeof row.a === "string" ? row.a.trim().slice(0, STATIC_LIMITS.answer) : "";
91
+ if (!question || !answer) continue;
92
+ out.push({
93
+ id: `himi-static-faq-${out.length}`,
94
+ categoryId: STATIC_CATEGORY.id,
95
+ question,
96
+ answer,
97
+ sortOrder: out.length,
98
+ status: "VISIBLE",
99
+ });
100
+ }
101
+ return out;
102
+ }
103
+
104
+ /** The site's pairs as a ready state, or undefined when there are none to fall back to. */
105
+ function staticFallback(state: FaqState): FaqState | undefined {
106
+ if (!staticQuestions.length) return undefined;
107
+ return ready({ ...state, categories: [STATIC_CATEGORY], questions: staticQuestions });
108
+ }
109
+
61
110
  function sortByWixOrder<T extends { id: string; sortOrder?: number }>(left: T, right: T): number {
62
111
  const a = Number.isFinite(left.sortOrder) ? left.sortOrder! : Number.POSITIVE_INFINITY;
63
112
  const b = Number.isFinite(right.sortOrder) ? right.sortOrder! : Number.POSITIVE_INFINITY;
@@ -101,7 +150,8 @@ function ready(state: FaqState): FaqState {
101
150
  const rows = sections.flatMap((section) => section.questions.map((question, index) => ({
102
151
  ...question,
103
152
  categoryTitle: section.title,
104
- categoryHeaderHidden: index !== 0,
153
+ // An untitled section (the static fallback's) has no header to show.
154
+ categoryHeaderHidden: index !== 0 || !section.title,
105
155
  })));
106
156
  return {
107
157
  ...state,
@@ -218,17 +268,28 @@ function commit(state: FaqState): FaqState {
218
268
  return state;
219
269
  }
220
270
 
221
- async function loadFaq(state: FaqState, arg?: { otherCategory?: string }): Promise<FaqState> {
271
+ async function loadFaq(state: FaqState, arg?: { otherCategory?: string; staticFaq?: unknown }): Promise<FaqState> {
222
272
  latestState = state;
273
+ // Only when the caller passed the param: `retry` is dispatched with no params and must keep
274
+ // the pairs the screen's onAppear delivered.
275
+ if (arg && "staticFaq" in arg) staticQuestions = parseStaticFaq(arg.staticFaq);
223
276
  const generation = ++loadGeneration;
224
277
  try {
225
278
  const [categories, questions] = await Promise.all([allCategories(), allQuestions()]);
226
279
  if (generation !== loadGeneration) return latestState;
227
280
  const otherTitle = String(arg?.otherCategory ?? latestState.otherTitle).trim() || latestState.otherTitle;
228
- return commit(ready({ ...latestState, categories, questions, otherTitle }));
281
+ const live = ready({ ...latestState, categories, questions, otherTitle });
282
+ // The Wix FAQ app answered but holds nothing a visitor can see — the site's own pairs,
283
+ // when it has any, instead of an empty state over a site whose FAQ is right there.
284
+ if (questions.length === 0) return commit(staticFallback(live) ?? live);
285
+ return commit(live);
229
286
  } catch (error) {
230
287
  if (generation !== loadGeneration) return latestState;
231
288
  const status = httpStatus(error);
289
+ // No FAQ app (428), no visitor access (403) or a failed read: a site whose FAQ is page
290
+ // content still has an FAQ to show, so its pairs replace the failure card.
291
+ const fallback = staticFallback(latestState);
292
+ if (fallback) return commit(fallback);
232
293
  return commit(failure(latestState, status === 403 ? "forbidden" : status === 428 ? "notInstalled" : "error"));
233
294
  }
234
295
  }
@@ -250,4 +311,4 @@ const svc = defineService("himiBrandedFaq", {
250
311
  },
251
312
  });
252
313
 
253
- export { svc, sectionsFor, httpStatus };
314
+ export { svc, sectionsFor, httpStatus, parseStaticFaq };