@wix/himalaya-cli 0.1470.0 → 0.1471.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.
@@ -2,6 +2,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import type { DerivedAppMeta } from "./app-module.js";
3
3
  import { type WixCachePolicy } from "./wix-cache-policy.js";
4
4
  import type { NetMocksFile, WixMockRule } from "./wix-gateway.js";
5
+ import { gateScreenIdProblem } from "../../ts/src/auth-gate-screen.js";
5
6
  import { type UnmappedPushDestination } from "../../ts/src/wix-push-links.js";
6
7
  import type { PushConfig, FlagConfig, AppControl, TabBarConfig, DrawerConfig, SidebarConfig, SplashConfig, LocaleConfig, NavKind, OrientationConfig, StreamPolicy } from "@wix/himalaya/core";
7
8
  export type { PushConfig, FlagConfig, AppControl, TabBarConfig, DrawerConfig, SidebarConfig, SplashConfig, LocaleConfig, NavKind, OrientationConfig, StreamPolicy };
@@ -37,6 +38,7 @@ export interface TelemetryConfig {
37
38
  export interface AuthConfigBlock {
38
39
  providerId: string;
39
40
  required: boolean;
41
+ gateScreenId?: string;
40
42
  config: Record<string, unknown>;
41
43
  }
42
44
  /** RFC 0010 (nav-modes amendment) — OTA back-affordance config. `enabled` gates
@@ -195,13 +197,19 @@ export interface DevGatewayConfig {
195
197
  * answers a miss with the same 404 and never calls the real capability. */
196
198
  unmatched?: "passthrough" | "fail";
197
199
  }
200
+ /** #4651 — the gate-screen existence rule lives import-free in core (the auth bake shares it). */
201
+ export { gateScreenIdProblem };
198
202
  /** Derive the OTA authConfig block from a defineApp auth block
199
203
  * ({ providerId, config }). `required` is read from `config.required === true`
200
- * and lifted to the top level; secret-ish keys are scrubbed. */
204
+ * and lifted to the top level, as is `config.gateScreenId` (#4651); secret-ish
205
+ * keys are scrubbed. When the app's screens are given, a `gateScreenId` naming none
206
+ * of them is DROPPED (the shell falls back to the stock login) and warned about —
207
+ * never thrown: this runs on every app-config serve and hot reload, and
208
+ * `himalaya-dev app validate` is where a bad id fails ([gateScreenIdProblem]). */
201
209
  export declare function deriveAuthConfig(auth: {
202
210
  providerId: string;
203
211
  config: Record<string, unknown>;
204
- } | undefined): AuthConfigBlock | undefined;
212
+ } | undefined, screenIds?: readonly string[]): AuthConfigBlock | undefined;
205
213
  /** Derive a baseline NavigationConfig from the loaded app's derived meta. */
206
214
  /** What `deriveNavigationConfig` needs to finish an app-level nav bundle ref.
207
215
  *
@@ -94,8 +94,16 @@ export declare const auth: {
94
94
  isAuthenticated(): Promise<boolean>;
95
95
  /** The current access/member token (or null when anonymous). */
96
96
  getAccessToken(): Promise<string | null>;
97
- /** Sign in (hosted Wix login → member token). Resolves with the new identity. */
98
- signIn(): Promise<WixIdentity>;
97
+ /** Sign in, resolving with the new identity. With a real provider bound to the app's session
98
+ * gate this presents the real sign-in as a full-screen cover over the current screen — the
99
+ * app's `auth.config.gateScreenId` screen if set, else the stock login, opened on `intent`
100
+ * (default `"login"`) — and resolves once the user is signed in. Dismissing it rejects with
101
+ * `err.code === "cancelled"`; a second call while one is up rejects `"sign_in_in_progress"`.
102
+ * The calling worker survives the sign-in. Before the provider is bound it rejects
103
+ * `sign_in_unavailable`, as on web; only a shell with no session gate keeps the dev seam. */
104
+ signIn(opts?: {
105
+ intent?: "login" | "signup";
106
+ }): Promise<WixIdentity>;
99
107
  /** Sign out — clears the member token (visitor session may persist). */
100
108
  signOut(): Promise<WixIdentity>;
101
109
  /** Mint/return a per-business signed instance for authorized Wix REST calls —
@@ -106,8 +114,8 @@ export declare const auth: {
106
114
  *
107
115
  * When the app provisions `real.auth`, it mints through the REAL Wix endpoint
108
116
  * (the same `one-app-session-web` mint the Owner/Spaces bundles used ad-hoc),
109
- * so the dev gateway records it; otherwise (and on any failure) it uses the
110
- * native dev seam. */
117
+ * so it is observed like any other Wix call; otherwise (and on any failure) it
118
+ * uses the native dev seam. */
111
119
  signedInstance(businessId: string, type?: WixSignedInstanceType): Promise<string | null>;
112
120
  };
113
121
  /** One business's scope, as `site.forSite(msid)` returns it.
@@ -177,7 +185,7 @@ export declare const site: {
177
185
  * Only reaches businesses the signed-in user owns: the header names a business, and
178
186
  * Wix refuses to mint an instance for one they do not. A consumer app (one Headless
179
187
  * clientId, one site) does not mint per-site at all, so this is inert there — see
180
- * docs/branded-lite-site-binding.md. */
188
+ * the branded-lite-site-binding guide. */
181
189
  forSite(msid: string): WixSiteScope;
182
190
  };
183
191
  export declare const monitoring: {
@@ -0,0 +1,10 @@
1
+ /** Why a `gateScreenId` value is not a usable screen id, or null when it is (or is absent). */
2
+ export declare function gateScreenIdShapeProblem(id: unknown): string | null;
3
+ /** Why `auth.config.gateScreenId` is not one of the app's exact screens, or null when it is (or is
4
+ * absent). One rule for every path — the served OTA authConfig, `app validate` and the auth bake:
5
+ * a release serves the exact screens (a templated route only for its build-time seed ids), and the
6
+ * OTA value overrides the baked one, so a templated gate id would mount a 404 gate in a release. */
7
+ export declare function gateScreenIdProblem(auth: {
8
+ providerId: string;
9
+ config: Record<string, unknown>;
10
+ } | undefined, screenIds: readonly string[]): string | null;
@@ -819,9 +819,24 @@ export interface ProdEnvironment {
819
819
  export interface AuthProvider {
820
820
  /** Stable id, e.g. "wix", "generic-oauth", "google", "<custom>". */
821
821
  providerId: string;
822
- /** Provider-specific configuration, opaque to core. */
822
+ /**
823
+ * Provider-specific configuration, opaque to core — except two keys core lifts to the top of
824
+ * the baked `auth.json` and the OTA `authConfig`:
825
+ *
826
+ * - `required: true` gates the app's root on an authenticated session.
827
+ * - `gateScreenId: "<screen id>"` (#4651) names a screen the app ships as its own auth gate. A
828
+ * required-auth launch shows it, rendered by the ordinary screen host as a full-screen root,
829
+ * instead of the stock login, and a worker's mid-session `wixPlatform.auth.signIn()` presents
830
+ * it as a cover. It drives sign-in through the `auth.*` actions (`auth.signIn` with
831
+ * `params.intent: "login" | "signup"` opens the provider's own login UI in that mode); the
832
+ * credential still never passes through app code, the floor `screens` documents.
833
+ */
823
834
  config: Record<string, unknown>;
824
835
  }
836
+ /** Validate the SHAPE of `auth.config.gateScreenId` (#4651): a non-empty string when present.
837
+ * Whether it names a screen the app ships is `gateScreenIdProblem`'s rule (the dev-server's app
838
+ * config, `himalaya-dev app validate` and the auth bake), which knows the app's screens. */
839
+ export declare function validateAuthGateScreen(auth: AuthProvider | undefined): void;
825
840
  export interface TelemetrySink {
826
841
  sinkId: string;
827
842
  emit(event: TelemetryEvent): void | Promise<void>;
@@ -7143,6 +7143,15 @@ function validateDistribution(d, where = "distribution") {
7143
7143
  }
7144
7144
  }
7145
7145
 
7146
+ // ../../core/ts/src/auth-gate-screen.ts
7147
+ function gateScreenIdShapeProblem(id) {
7148
+ if (id === void 0) return null;
7149
+ if (typeof id !== "string" || id.trim().length === 0) {
7150
+ return `defineApp: auth.config.gateScreenId must be a non-empty screen id string, got ${JSON.stringify(id)}.`;
7151
+ }
7152
+ return null;
7153
+ }
7154
+
7146
7155
  // ../../core/data-resources/src/base44/schema.ts
7147
7156
  var BASE44_FIELD_TYPES = ["string", "number", "integer", "boolean", "array", "object"];
7148
7157
  var BASE44_STRING_FORMATS = ["date", "date-time", "time", "email", "uri", "uuid", "file", "richtext"];
@@ -7462,6 +7471,10 @@ function resolveOrientationEnvelope(env) {
7462
7471
  tablet: env?.tablet?.length ? env.tablet : DEFAULT_ORIENTATION_ENVELOPE.tablet
7463
7472
  };
7464
7473
  }
7474
+ function validateAuthGateScreen(auth) {
7475
+ const problem = gateScreenIdShapeProblem(auth?.config?.gateScreenId);
7476
+ if (problem) throw new Error(problem);
7477
+ }
7465
7478
  function dataSchemaScopeOf(entity) {
7466
7479
  if (entity.scope === "app" || entity.scope === "principal") return entity.scope;
7467
7480
  return entity.encrypted ? "principal" : "app";
@@ -7702,6 +7715,7 @@ function validateConfigShape(config) {
7702
7715
  validateDistribution(config.distribution, "defineApp: distribution");
7703
7716
  }
7704
7717
  validateLadderConfig(config);
7718
+ validateAuthGateScreen(config.auth);
7705
7719
  if (config.tier5Cap) {
7706
7720
  const c = config.tier5Cap;
7707
7721
  if (c.percentageOfActiveScreens < 0 || c.percentageOfActiveScreens > 100) {
@@ -14687,6 +14701,7 @@ export {
14687
14701
  tonalElevationAlpha,
14688
14702
  validate,
14689
14703
  validateAll,
14704
+ validateAuthGateScreen,
14690
14705
  validateBase44Entities,
14691
14706
  validateDistribution,
14692
14707
  validateLaunchManifest,
@@ -200,9 +200,24 @@ var session = {
200
200
  return { present: r?.present ?? false, msid: r?.msid ?? null };
201
201
  }
202
202
  };
203
- async function authTransition(method) {
204
- await call2(method);
203
+ var AUTH_REFUSAL_CODES = /* @__PURE__ */ new Set(["cancelled", "sign_in_in_progress", "sign_in_unavailable"]);
204
+ async function authTransition(method, params) {
205
+ const r = await call2(method, params).catch((e) => {
206
+ const message = e instanceof Error ? e.message : String(e);
207
+ throw AUTH_REFUSAL_CODES.has(message) ? Object.assign(e instanceof Error ? e : new Error(message), { code: message }) : e;
208
+ });
209
+ if (typeof r?.error === "string" && r.error.length > 0) {
210
+ throw Object.assign(new Error(r.error), { code: r.error });
211
+ }
205
212
  dropConductFlights();
213
+ if (method === "authSignIn" && typeof r?.authenticated === "boolean") {
214
+ return {
215
+ authenticated: r.authenticated,
216
+ type: r.type ?? (r.authenticated ? "member" : "anonymous"),
217
+ userId: r.userId || null,
218
+ email: r.email || null
219
+ };
220
+ }
206
221
  return auth.getIdentity();
207
222
  }
208
223
  var auth = {
@@ -225,9 +240,15 @@ var auth = {
225
240
  const r = await call2("authAccessToken");
226
241
  return r?.token && r.token.length > 0 ? r.token : null;
227
242
  },
228
- /** Sign in (hosted Wix login → member token). Resolves with the new identity. */
229
- signIn() {
230
- return authTransition("authSignIn");
243
+ /** Sign in, resolving with the new identity. With a real provider bound to the app's session
244
+ * gate this presents the real sign-in as a full-screen cover over the current screen — the
245
+ * app's `auth.config.gateScreenId` screen if set, else the stock login, opened on `intent`
246
+ * (default `"login"`) — and resolves once the user is signed in. Dismissing it rejects with
247
+ * `err.code === "cancelled"`; a second call while one is up rejects `"sign_in_in_progress"`.
248
+ * The calling worker survives the sign-in. Before the provider is bound it rejects
249
+ * `sign_in_unavailable`, as on web; only a shell with no session gate keeps the dev seam. */
250
+ signIn(opts) {
251
+ return authTransition("authSignIn", { intent: opts?.intent ?? "login" });
231
252
  },
232
253
  /** Sign out — clears the member token (visitor session may persist). */
233
254
  signOut() {
@@ -241,8 +262,8 @@ var auth = {
241
262
  *
242
263
  * When the app provisions `real.auth`, it mints through the REAL Wix endpoint
243
264
  * (the same `one-app-session-web` mint the Owner/Spaces bundles used ad-hoc),
244
- * so the dev gateway records it; otherwise (and on any failure) it uses the
245
- * native dev seam. */
265
+ * so it is observed like any other Wix call; otherwise (and on any failure) it
266
+ * uses the native dev seam. */
246
267
  async signedInstance(businessId, type = "User") {
247
268
  if (isMemberReal("auth")) {
248
269
  try {
@@ -341,7 +362,7 @@ var site = {
341
362
  * Only reaches businesses the signed-in user owns: the header names a business, and
342
363
  * Wix refuses to mint an instance for one they do not. A consumer app (one Headless
343
364
  * clientId, one site) does not mint per-site at all, so this is inert there — see
344
- * docs/branded-lite-site-binding.md. */
365
+ * the branded-lite-site-binding guide. */
345
366
  forSite(msid) {
346
367
  const scoped = (msid ?? "").trim();
347
368
  return {
package/dist/cli.mjs CHANGED
@@ -8127,7 +8127,6 @@ function resolveTranslation(value, catalog3, sourceCatalog) {
8127
8127
  var BINDING_SIGILS, BINDING_SIGIL_RE, TRANSLATION_PREFIX, TRANSLATION_KEY_RE;
8128
8128
  var init_bindings = __esm({
8129
8129
  "../../core/ts/src/bindings.ts"() {
8130
- "use strict";
8131
8130
  BINDING_SIGILS = ["service", "flag", "item", "t"];
8132
8131
  BINDING_SIGIL_RE = new RegExp(
8133
8132
  `^\\$(?:${BINDING_SIGILS.filter((s2) => s2 !== "t").join("|")})\\b|^\\$t:`
@@ -31137,6 +31136,28 @@ var init_l10n = __esm({
31137
31136
  }
31138
31137
  });
31139
31138
 
31139
+ // ../../core/ts/src/auth-gate-screen.ts
31140
+ function gateScreenIdShapeProblem(id) {
31141
+ if (id === void 0) return null;
31142
+ if (typeof id !== "string" || id.trim().length === 0) {
31143
+ return `defineApp: auth.config.gateScreenId must be a non-empty screen id string, got ${JSON.stringify(id)}.`;
31144
+ }
31145
+ return null;
31146
+ }
31147
+ function gateScreenIdProblem(auth, screenIds) {
31148
+ const id = auth?.config?.gateScreenId;
31149
+ if (id === void 0) return null;
31150
+ const shape = gateScreenIdShapeProblem(id);
31151
+ if (shape) return shape;
31152
+ const gateId = id;
31153
+ if (screenIds.includes(gateId)) return null;
31154
+ return `defineApp: auth.config.gateScreenId is "${gateId}", which is not one of the app's exact screens \u2014 a required-auth launch would have no login to show (a release serves exact screens, so a templated route 404s). Name an exact screen, or drop gateScreenId to use the stock login.`;
31155
+ }
31156
+ var init_auth_gate_screen = __esm({
31157
+ "../../core/ts/src/auth-gate-screen.ts"() {
31158
+ }
31159
+ });
31160
+
31140
31161
  // ../../core/ts/src/wix-push-links.ts
31141
31162
  function replacementPingFamily(name) {
31142
31163
  return RETIRED_PING_FAMILIES.has(name) ? DEFAULT_PING_APP_NAME : void 0;
@@ -31328,17 +31349,21 @@ function deriveTelemetryConfig(app) {
31328
31349
  }
31329
31350
  return cfg;
31330
31351
  }
31331
- function deriveAuthConfig(auth) {
31352
+ function deriveAuthConfig(auth, screenIds) {
31332
31353
  if (!auth?.providerId) return void 0;
31333
31354
  const config = {};
31334
31355
  for (const [key3, value] of Object.entries(auth.config ?? {})) {
31335
- if (key3 === "required") continue;
31356
+ if (key3 === "required" || key3 === "gateScreenId") continue;
31336
31357
  if (AUTH_SECRET_KEY_RE.test(key3)) continue;
31337
31358
  config[key3] = value;
31338
31359
  }
31360
+ const gateScreenId = auth.config?.gateScreenId;
31361
+ const problem = typeof gateScreenId === "string" && screenIds ? gateScreenIdProblem(auth, screenIds) : null;
31362
+ if (problem) console.warn(`[app-config] ${problem} \u2014 serving the stock login instead.`);
31339
31363
  return {
31340
31364
  providerId: auth.providerId,
31341
31365
  required: auth.config?.required === true,
31366
+ ...typeof gateScreenId === "string" && gateScreenId.length > 0 && !problem ? { gateScreenId } : {},
31342
31367
  config
31343
31368
  };
31344
31369
  }
@@ -31448,7 +31473,7 @@ function deriveNavigationConfig(app, bundleCtx) {
31448
31473
  if (app.homeScreenId) config.homeScreenId = app.homeScreenId;
31449
31474
  if (app.label) config.name = app.label;
31450
31475
  config.backAffordance = { enabled: true };
31451
- const authConfig = deriveAuthConfig(app.auth);
31476
+ const authConfig = deriveAuthConfig(app.auth, app.screenIds);
31452
31477
  if (authConfig !== void 0) config.authConfig = authConfig;
31453
31478
  if (app.iap !== void 0) config.iapConfig = app.iap;
31454
31479
  if (app.cashier !== void 0) config.cashierConfig = app.cashier;
@@ -31539,6 +31564,7 @@ var init_app_config = __esm({
31539
31564
  "../../core/dev-server/src/app-config.ts"() {
31540
31565
  init_wix_cache_policy();
31541
31566
  init_l10n();
31567
+ init_auth_gate_screen();
31542
31568
  init_preview_surfaces();
31543
31569
  init_wix_push_links();
31544
31570
  SHARED_WIX_SENTRY_DSN = "https://5dfa4f23057c43149b6219c07d177472@sentry.io/84240";
@@ -32305,6 +32331,7 @@ var init_define_app = __esm({
32305
32331
  "../../core/ts/src/define-app.ts"() {
32306
32332
  init_locale();
32307
32333
  init_distribution();
32334
+ init_auth_gate_screen();
32308
32335
  init_schema2();
32309
32336
  init_base44_entities();
32310
32337
  init_src3();
@@ -5209,7 +5209,7 @@
5209
5209
  "name": "auth",
5210
5210
  "kind": "const",
5211
5211
  "jsdoc": "",
5212
- "decl": "export const auth = {\n /** Current identity (visitor vs member vs owner) + who. */\n async getIdentity(): Promise<WixIdentity> {\n const r = await call<Partial<WixIdentity>>(\"authIdentity\");\n return {\n authenticated: r?.authenticated ?? false,\n type: (r?.type as WixIdentityType) ?? \"anonymous\",\n userId: r?.userId ?? null,\n email: r?.email ?? null,\n };\n },\n /** True when a member/owner is signed in. */\n async isAuthenticated(): Promise<boolean> {\n return (await auth.getIdentity()).authenticated;\n },\n /** The current access/member token (or null when anonymous). */\n async getAccessToken(): Promise<string | null> {\n const r = await call<{ token?: string }>(\"authAccessToken\");\n return r?.token && r.token.length > 0 ? r.token : null;\n },\n /** Sign in (hosted Wix login → member token). Resolves with the new identity. */\n signIn(): Promise<WixIdentity> { return authTransition(\"authSignIn\"); },\n /** Sign out — clears the member token (visitor session may persist). */\n signOut(): Promise<WixIdentity> { return authTransition(\"authSignOut\"); },\n /** Mint/return a per-business signed instance for authorized Wix REST calls —\n * the ONE canonical worker-facing signed-instance accessor (RFC 0043 D3). A\n * worker never hand-rolls the mint HTTP; it calls this. `type` is the full Wix\n * identity matrix (owner=`User`, member, visitor, plus the Velo-scoped\n * variants) — mirrors @himalaya/wix-auth `SignedInstanceType`.\n *\n * When the app provisions `real.auth`, it mints through the REAL Wix endpoint\n * (the same `one-app-ses\n/* … */"
5212
+ "decl": "export const auth = {\n /** Current identity (visitor vs member vs owner) + who. */\n async getIdentity(): Promise<WixIdentity> {\n const r = await call<Partial<WixIdentity>>(\"authIdentity\");\n return {\n authenticated: r?.authenticated ?? false,\n type: (r?.type as WixIdentityType) ?? \"anonymous\",\n userId: r?.userId ?? null,\n email: r?.email ?? null,\n };\n },\n /** True when a member/owner is signed in. */\n async isAuthenticated(): Promise<boolean> {\n return (await auth.getIdentity()).authenticated;\n },\n /** The current access/member token (or null when anonymous). */\n async getAccessToken(): Promise<string | null> {\n const r = await call<{ token?: string }>(\"authAccessToken\");\n return r?.token && r.token.length > 0 ? r.token : null;\n },\n /** Sign in, resolving with the new identity. With a real provider bound to the app's session\n * gate this presents the real sign-in as a full-screen cover over the current screen — the\n * app's `auth.config.gateScreenId` screen if set, else the stock login, opened on `intent`\n * (default `\"login\"`) — and resolves once the user is signed in. Dismissing it rejects with\n * `err.code === \"cancelled\"`; a second call while one is up rejects `\"sign_in_in_progress\"`.\n * The calling worker survives the sign-in. Before the provider is bound it rejects\n * `sign_in_unavailable`, as on web; only a shell with no session gate keeps the dev seam. */\n signIn(opts?: { intent?: \"login\" | \"signup\" }): Promise<WixIdentity> {\n return authTransition(\"authSignIn\", { intent: opts?.intent ?? \"login\" });\n },\n /** Sign out — clears the member token (visitor session may persist). */\n signOut(): Promise<WixIdentity> { return authTransition(\"authSignOut\"); },\n /** Mint/return a per-business signed instance for authorized Wix REST calls —\n * the ONE canonical worker-facing signed-instance accessor (RFC 0043 D3). A\n * worker never hand-rolls the mint HTTP; it calls this. `type` is the full Wix\n * identity matrix (owner=`User`, member, visitor, plus the Velo-scoped\n * variants) — mirrors @himalaya/wix-auth `SignedInstanceType`.\n *\n * When the app provisions `real.auth`, it mints through the REAL Wix endpoint\n * (the same `one-app-session-web` mint the Owner/Spaces bundles used ad-hoc),\n * so it is observed like any other Wix call; otherwise (and on any failure) it\n * uses the native dev seam. */\n async signedInstance(businessId: string, type: WixSignedInstanceType = \"User\"): Promise<string | null> {\n if (isMemberReal(\"auth\")) {\n try {\n const r = await realWix<{ signedInstance?: string }>(\n \"GET\",\n `https://apps.wix.com/_api/one-app-session-web/v3/businesses/${encodeURIComponent(businessId)}/signed-instance?signed_instance_type=${type}`,\n );\n if (typeof r?.signedInstance === \"string\" && r.signedInstance.length > 0) return r.signedInstance;\n } catch { /* fall through to the dev seam */ }\n }\n const r = await call<{ signedInstance?: string }>(\"authSignedInstance\", { businessId, type });\n return r?.signedInstance && r.signedInstance.length > 0 ? r.signedInstance : null;\n },\n};"
5213
5213
  },
5214
5214
  {
5215
5215
  "name": "WixSiteScope",
@@ -5227,7 +5227,7 @@
5227
5227
  "name": "site",
5228
5228
  "kind": "const",
5229
5229
  "jsdoc": "",
5230
- "decl": "export const site = {\n /** Scope to a business by metaSiteId; resolves after the per-site instance mints. */\n setActiveSite(msid: string): Promise<{ ok: boolean; msid: string }> { return scopeToSite(msid); },\n /** The metaSiteId currently scoped (\"\" if none). */\n async activeSite(): Promise<string> {\n const r = await call<{ msid?: string }>(\"siteActive\");\n return r?.msid ?? \"\";\n },\n /** The customer's business/site info (identity + locale + contact) from Wix Site\n * Properties v4 (RFC 0048). Unlike bi/experiments, this is a plain REST GET: it\n * rides the wixApi transport — the same chokepoint the dev gateway observes /\n * mocks and prod binds — scoped to the active site's `User` signed instance, so\n * there is no native dev seam and no `real.site` flag. Returns `null` when no Wix\n * transport is installed (a non-Wix app) or the call fails, so callers degrade\n * gracefully. The ONE door for business info (RFC 0043) — a worker should call\n * this rather than hand-rolling `wixApi.call(\".../site-properties/...\")`. */\n async getBusinessInfo(): Promise<WixBusinessInfo | null> {\n if (typeof wpHost().wixApi?.call !== \"function\") return null;\n try {\n const res = await realWix<{ properties?: RawSiteProps } & RawSiteProps>(\n \"GET\", \"/site-properties/v4/properties\",\n );\n return parseBusinessInfo(res);\n } catch {\n return null;\n }\n },\n\n /** A handle that runs calls against ONE business, without moving the active site.\n *\n * `setActiveSite` re-scopes the whole process — every later call, every worker, the\n/* … */"
5230
+ "decl": "export const site = {\n /** Scope to a business by metaSiteId; resolves after the per-site instance mints. */\n setActiveSite(msid: string): Promise<{ ok: boolean; msid: string }> { return scopeToSite(msid); },\n /** The metaSiteId currently scoped (\"\" if none). */\n async activeSite(): Promise<string> {\n const r = await call<{ msid?: string }>(\"siteActive\");\n return r?.msid ?? \"\";\n },\n /** The customer's business/site info (identity + locale + contact) from Wix Site\n * Properties v4 (RFC 0048). Unlike bi/experiments, this is a plain REST GET: it\n * rides the wixApi transport — the same chokepoint the dev gateway observes /\n * mocks and prod binds — scoped to the active site's `User` signed instance, so\n * there is no native dev seam and no `real.site` flag. Returns `null` when no Wix\n * transport is installed (a non-Wix app) or the call fails, so callers degrade\n * gracefully. The ONE door for business info (RFC 0043) — a worker should call\n * this rather than hand-rolling `wixApi.call(\".../site-properties/...\")`. */\n async getBusinessInfo(): Promise<WixBusinessInfo | null> {\n if (typeof wpHost().wixApi?.call !== \"function\") return null;\n try {\n const res = await realWix<{ properties?: RawSiteProps } & RawSiteProps>(\n \"GET\", \"/site-properties/v4/properties\",\n );\n return parseBusinessInfo(res);\n } catch {\n return null;\n }\n },\n\n /** A handle that runs calls against ONE business, without moving the active site.\n *\n * `setActiveSite` re-scopes the whole process — every later call, every worker, the\n * conducted cache and the entity cache all move with it. That is right for \"the owner\n * picked a business to manage\", and wrong for \"show me tonight's covers across all\n * three restaurants\": a rollup needs N businesses readable AT ONCE, and switching\n * between them to read each one serially is both slow and destructive of the scope the\n * user is actually in.\n *\n * The returned object IS a `WixSiteScope`, so it can be handed straight to a generated\n * client as its options bag — `searchOrders(req, site.forSite(msid))` — and it also\n * carries `call` for a raw path the typed clients do not cover. Fan several out with\n * `Promise.all` and each one keeps its own business.\n *\n * Only reaches businesses the signed-in user owns: the header names a business, and\n * Wix refuses to mint an instance for one they do not. A consumer app (one Headless\n * clientId, one site) does not mint per-site at all, so this is inert there — see\n * the branded-lite-site-binding guide. */\n forSite(msid: string): WixSiteScope {\n const scoped = (msid ?? \"\").trim();\n return {\n site: scoped,\n async call<T>(method: string, path: string, body?: unknown): Promise<T> {\n const api = wpHost().wixApi;\n if (!api) throw new Error(\"wixApi bridge unavailable\");\n // A blank msid would silently read the AMBIENT business, which is the one\n // failure mode a cross-site rollup cannot tolerate: it would attribute one\n // business's rows to another with nothing in the response to show for it.\n if (!scoped) throw new Error(\"site.forSite: a metaSiteId is required\");\n return (await api.call(method, path, body, { \"wix-site-id\": scoped })) as T;\n },\n };\n },\n};"
5231
5231
  },
5232
5232
  {
5233
5233
  "name": "monitoring",
@@ -54,4 +54,4 @@ the SDK sources, so it matches the shipped SDKs.
54
54
  - [`ui`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fui?format=md) — Imperative popup capability. 2 methods · not in Brandy.
55
55
  - [`wix_cashier`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fwix_cashier?format=md) — Native cashier. 2 methods.
56
56
  - [`wix_chat`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fwix_chat?format=md) — Host-owned realtime Chat event subscriptions. 2 methods.
57
- - [`wix_platform`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fwix_platform?format=md) — Wix Platform SDK, worker-facing bindings. 30 methods · not in Brandy.
57
+ - [`wix_platform`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fwix_platform?format=md) — Wix Platform SDK, worker-facing bindings. 27 methods · not in Brandy.
@@ -58,14 +58,14 @@ can't be built (see §Fail-closed). One place; all platforms read the baked outp
58
58
 
59
59
  ## Step 2 — bake → `auth.json`
60
60
 
61
- `tools/auth-bake/bake-auth.ts` reads `config.ts`, lifts `required` to the top level, **scrubs any
61
+ `tools/auth-bake/bake-auth.ts` reads `config.ts`, lifts `required` (and `gateScreenId`, below) to the top level, **scrubs any
62
62
  `secret`/`private` key**, and writes `auth.json` into the iOS bundle, Android assets, and (when
63
63
  present) the macOS bundle resources:
64
64
 
65
65
  ```
66
66
  node --import tsx tools/auth-bake/bake-auth.ts apps/<app> --ios apps/<app>/ios/<target> --android apps/<app>/android/<module> --macos apps/<app>/macos/<target>
67
67
  ```
68
- `auth.json` = `{ providerId, required, config }`. Read by `TemplateAppBoot.loadBakedAuth()` (iOS
68
+ `auth.json` = `{ providerId, required, gateScreenId?, config }`. Read by `TemplateAppBoot.loadBakedAuth()` (iOS
69
69
  and macOS) / `TemplateAuth.boot()` (Android).
70
70
 
71
71
  ## Step 3 — wire the gate (per platform)
@@ -76,13 +76,16 @@ and macOS) / `TemplateAuth.boot()` (Android).
76
76
  instantiate via `AuthProviderRegistry.shared.instantiate(providerId:config:)`, `sessionGate.bind(to: provider)`,
77
77
  set `sessionGate.formMode = .emailEntry` for native, and **bind the transport**:
78
78
  `wixApiCapability = WixApiCapability(authProvider: provider, mobileContext: …)`.
79
- - The SwiftUI root observes `sessionGate.state` + `authRequired` → shows `AuthGateView` until authenticated.
79
+ - The SwiftUI root wraps its content in the core `AuthGateRoot(gate:required:gateScreen:content:)`, which
80
+ shows the gate until `sessionGate.state` is authenticated (the stock `AuthGateView`, or the app's own gate
81
+ screen — below) and presents a worker's mid-session sign-in over the content.
80
82
 
81
83
  ### Android
82
84
  - Gradle: add the `:himalaya-wix-auth` module (settings + `implementation(project(":himalaya-wix-auth"))`).
83
85
  - In `Application.onCreate()`: `HimalayaWixAuth.register()` **before** `TemplateAuth.boot(applicationContext)`
84
86
  (which reads `assets/auth.json`, instantiates via the registry, and `gate.bind(provider, scope)`).
85
- - The root composition observes `TemplateAuth.gate` + `TemplateAuth.required` → shows `AuthGateScreen`.
87
+ - The root composition calls the core `AuthGateRoot(gate, required, gateScreen, content)` with
88
+ `TemplateAuth.gate` + `TemplateAuth.required` — the stock `AuthGateScreen` or the app's own gate screen.
86
89
  - Bind the transport where you make Wix calls: `WixApiCapability(authProvider = provider)`.
87
90
 
88
91
  ### macOS
@@ -154,6 +157,55 @@ their own message; the primary button sends a new request, and the method picker
154
157
  factor at any time. Dev seam accounts: `2fa-push@himalaya.dev` (approves) and
155
158
  `2fa-push-deny@himalaya.dev` (declines), password `Passw0rd!`.
156
159
 
160
+ ## Your own gate screen and mid-session sign-in (`gateScreenId`, `auth.signIn`)
161
+
162
+ Name a screen the app ships as its auth gate, and the stock login steps aside:
163
+
164
+ ```ts
165
+ const native = WixNativeAuthProvider({ mode: "consumer", clientId: "…" });
166
+ // …
167
+ auth: { ...native, config: { ...native.config, required: true, gateScreenId: "welcome" } },
168
+ ```
169
+
170
+ (The provider factories forward only their own keys, so `required` and `gateScreenId` go on the
171
+ returned block's `config`, which is what the bake and the OTA derivation read.)
172
+
173
+ `defineApp` checks the shape; the app-config derivation drops, and the auth bake refuses, a `gateScreenId`
174
+ that is not one of the app's exact screens (a templated route is never a gate screen: a release serves
175
+ exact screens). It is lifted next to `required` in `auth.json` and the OTA `authConfig`, read with
176
+ `AuthBoot.gateScreenId(baked:ota:)` (Kotlin: `AuthBoot.gateScreenId(baked, ota)`) into
177
+ `SessionGate.gateScreenId`. A required-auth launch then shows that screen — the ordinary screen host,
178
+ worker and bindings included, as a full-screen root with no tab bar. It must be an exact screen
179
+ (a release serves exact screens): the auth bake and the app validation gate refuse anything else.
180
+
181
+ The screen drives sign-in only through `auth.*` actions, which `sessionGate.registerAuthActions()`
182
+ installs on the dispatcher — the credential never passes through app code:
183
+
184
+ | Action | Does |
185
+ |---|---|
186
+ | `auth.signIn` `{ intent: "login" \| "signup" }` | opens the provider's own login in that mode — wix-native: the in-app form in sign-in / register mode, over the gate screen (with a Back, `auth_gate_back`); a hosted provider: its hosted page (`intent` is a hint) |
187
+ | `auth.cancel` | closes a mid-session sign-in (the worker's call rejects `cancelled`), else steps back to the gate screen |
188
+ | `auth.socialSignIn`, `auth.setFormMode`, `auth.forgotPassword`, `auth.continueWithEmail`, biometric verbs | the existing set, routed to the gate |
189
+
190
+ The credential-carrying actions (`auth.signInWithPassword`, `auth.register`, `auth.submit2FA`) are
191
+ deliberately **not** routed from screens: a screen's controls can bind their params to anything, so a
192
+ password typed into an app screen would pass through app code. A gate screen opens the provider's own
193
+ form with `auth.signIn`, and only that native form supplies a credential. The gate actions accept a
194
+ dispatch from a screen's own controls only — not from a worker, an agent, or a WebView page.
195
+
196
+ **Mid-session sign-in.** With a real provider bound to the gate, a worker's
197
+ `wixPlatform.auth.signIn({ intent })` presents the sign-in as a full-screen cover over the current screen —
198
+ the `gateScreenId` screen if set, else the stock gate — and resolves with the new identity once the gate is
199
+ authenticated. Closing it (`auth_signin_cover_close`) rejects with `err.code === "cancelled"`; a second
200
+ call while one is up rejects `sign_in_in_progress`. The calling worker survives. The cover is part of
201
+ `AuthGateRoot`, and while it is mounted it is what `capability.wix_platform.authSignIn` routes to: with no
202
+ provider bound to the gate (yet, on a cold launch, or at all) it rejects `sign_in_unavailable` — never the
203
+ dev seam's demo member. Only a shell with no `AuthGateRoot` keeps the dev seam; on web, so does a host
204
+ with no live Wix member bridge.
205
+
206
+ Shells are copied when an app is scaffolded: a new app's shell calls `AuthGateRoot`, while an app
207
+ scaffolded before keeps its inline stock gate until its root switches to the helper.
208
+
157
209
  ## Fail-closed (`required: true`)
158
210
 
159
211
  If a required-auth app can't build its provider (unknown `providerId`, missing config key), boot sets a
@@ -2,7 +2,7 @@
2
2
 
3
3
  `@himalaya/wix-platform` is the T5-worker-facing surface of the Wix Platform SDK. It is
4
4
  **platform-agnostic** — the same JS runs on iOS, Android, macOS and web, with **one documented
5
- exception**: on web, `auth.signIn()` rejects rather than resolving (see
5
+ exception**: `auth.signIn()` can reject, with an `err.code`, on every platform (see
6
6
  [Real vs dev seam](#real-vs-dev-seam-and-best-effort) below). Each
7
7
  member dispatches to the native `wix_platform` capability (with an on-device dev seam);
8
8
  `bi`, `monitoring`, `session` and `auth` take their real backend when the app enables them
@@ -76,22 +76,37 @@ await monitoring.captureError("thing failed", { code: "E1" });
76
76
  `real.deeplink` are ignored.
77
77
  - `bi`, `monitoring`, and `auth` route through their native backends (Frog sink, Sentry
78
78
  sink, `WixNativeAuthProvider`) rather than raw `wixApi`, but the calling contract is
79
- identical — **except `auth.signIn()` on web**, below.
79
+ identical — **except `auth.signIn()`, which can reject on every platform**, below.
80
80
 
81
- ### `auth.signIn()` on web is the one member that rejects
81
+ ### `auth.signIn({ intent })` on a real provider
82
82
 
83
- `auth.signIn()` is the **native** hosted-login path. On web it starts no session and
84
- **rejects with `sign_in_unavailable`** in both of the cases you will actually meet: when no
85
- member session can be started, and when the member bridge is unreachable. It resolves only
86
- when that bridge already holds a member, in which case it reports who.
83
+ On iOS, macOS and Android, when the shell has a real provider bound to its session gate (an app on the
84
+ core `AuthGateRoot`), `auth.signIn({ intent: "login" | "signup" })` presents the real sign-in as a
85
+ full-screen cover over the current screen and resolves with the signed-in identity. Closing it rejects with
86
+ `err.code === "cancelled"`, and a second concurrent call rejects `sign_in_in_progress` — so handle the
87
+ rejection. Before the boot has bound the provider (or in such a shell with none) it rejects
88
+ `sign_in_unavailable`; only a shell with no core `AuthGateRoot` keeps the dev seam's demo member. See
89
+ [wix-login-integration](wix-login-integration.md#your-own-gate-screen-and-mid-session-sign-in-gatescreenid-authsignin).
87
90
 
88
- So this is the one member where the "never throws" rule above does not hold, and a worker
89
- must handle it:
91
+ ### `auth.signIn()` is the one member that rejects
92
+
93
+ `auth.signIn()` is the one member where the "never throws" rule above does not hold — on
94
+ native and on web alike, and always with an `err.code`:
95
+
96
+ - `cancelled` — the user closed the sign-in (native).
97
+ - `sign_in_in_progress` — another sign-in is already up (native).
98
+ - `sign_in_unavailable` — no sign-in can be shown: on native, before the provider is bound or
99
+ where the shell cannot present one; on web, always unless the member bridge already holds a
100
+ member (then it resolves and reports who). Web starts no session itself: it rejects both
101
+ when no member session can be started and when the member bridge is unreachable.
102
+
103
+ So a worker must handle it, and can branch on the code:
90
104
 
91
105
  ```js
92
106
  try {
93
- await auth.signIn();
94
- } catch {
107
+ await auth.signIn({ intent: "login" });
108
+ } catch (err) {
109
+ if (err.code === "cancelled") return; // the user said no — leave the screen as it was
95
110
  // "sign-in isn't available here" — render a real state, not a dead button.
96
111
  }
97
112
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1470.0",
3
+ "version": "0.1471.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": [
@@ -71,5 +71,5 @@
71
71
  "artifactId": "himalaya-cli"
72
72
  }
73
73
  },
74
- "falconPackageHash": "e9bd938455d5133edf89ef9ba48bc6e52cc885a2d0cf82d2a56ea5bb"
74
+ "falconPackageHash": "cd4d5ceeaf1823831031129d85d33f4d35a9aa4e5bfd8804fe964fe1"
75
75
  }