zitejs 0.9.104 → 0.9.106

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.
@@ -0,0 +1,279 @@
1
+ "use strict";
2
+ /**
3
+ * Allocation of the identifiers `generateDbTs` emits.
4
+ *
5
+ * Every accessor on the generated `zite` object, and every field key on a
6
+ * record type, has to be unique — and `generateSchema` used to choose them with
7
+ * `toSdkName` and no memory at all. Two consequences, both seen in the wild:
8
+ *
9
+ * - A table named "Notifications" allocated `notifications`, which
10
+ * `generateDbTs` also emits for the platform's notifications client. The
11
+ * sentinel is written last, so it won: the table lost its client entirely
12
+ * and typed as `{ create(params: NotificationsCreateParams) }`.
13
+ * - Two fields whose display names normalize alike both claimed one accessor,
14
+ * and the second silently overwrote the first in the emitted type. No
15
+ * error, no diagnostic — just a column you could no longer read. Zite 1.0's
16
+ * generator de-duplicated with numeric suffixes; 2.0 dropped that.
17
+ *
18
+ * `allocateSchemaNames` is idempotent: a schema whose names are already unique
19
+ * and unreserved allocates to itself. That is what lets `generateDbTs` run it
20
+ * defensively over its input — a schema file written before this existed
21
+ * repairs itself on the next generate instead of emitting TypeScript that
22
+ * cannot compile.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.RESERVED_FIELD_ACCESSORS = exports.RESERVED_IDENTIFIERS = exports.RESERVED_TABLE_ACCESSORS = void 0;
26
+ exports.toPascalCase = toPascalCase;
27
+ exports.toCamelCase = toCamelCase;
28
+ exports.toSdkName = toSdkName;
29
+ exports.keepValidSdkName = keepValidSdkName;
30
+ exports.allocateSchemaNames = allocateSchemaNames;
31
+ exports.normalizeSchemaNames = normalizeSchemaNames;
32
+ exports.describeRenames = describeRenames;
33
+ // The case helpers live here rather than in lib.ts so this module has no import
34
+ // back into it — `lib.ts` needs the allocator, and a cycle between the two is
35
+ // avoidable. `lib.ts` re-exports them, so the published surface is unchanged.
36
+ function toPascalCase(name) {
37
+ const pascal = name
38
+ .replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase())
39
+ .replace(/^(.)/, (_, c) => c.toUpperCase())
40
+ // Drop leftover non-alphanumerics (e.g. a trailing ">" from "</p>") so the
41
+ // sdkName is always a valid identifier in .zite/db.ts.
42
+ .replace(/[^a-zA-Z0-9]+/g, "");
43
+ if (!pascal)
44
+ return "_";
45
+ return /^[0-9]/.test(pascal) ? `_${pascal}` : pascal; // can't start with a digit
46
+ }
47
+ function toCamelCase(name) {
48
+ const pascal = toPascalCase(name);
49
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
50
+ }
51
+ /**
52
+ * camelCase for a FRESH sdkName, reading acronym runs as words: "VIP" ->
53
+ * "vip", "APIKey" -> "apiKey" (plain toCamelCase yields "vIP" / "aPIKey").
54
+ *
55
+ * Deliberately not folded into toCamelCase: that also derives endpoint
56
+ * identifiers from existing filenames on every generate, where normalizing
57
+ * would rename a working app's `api.sendSMS`. This is only for names being
58
+ * chosen for the first time — generateSchema preserves existing sdkNames.
59
+ */
60
+ function toSdkName(name) {
61
+ const deAcronymed = name
62
+ .replace(/([A-Z]+)([A-Z][a-z])/g, (_, run, next) => run.charAt(0) + run.slice(1).toLowerCase() + next)
63
+ .replace(/([A-Z])([A-Z]+)/g, (_, first, rest) => first + rest.toLowerCase());
64
+ return toCamelCase(deAcronymed);
65
+ }
66
+ /**
67
+ * Accessors `generateDbTs` puts on the `zite` object itself, after the tables.
68
+ * A table that allocates one of these produces a duplicate key.
69
+ */
70
+ exports.RESERVED_TABLE_ACCESSORS = [
71
+ "sql",
72
+ "notifications",
73
+ "auth",
74
+ ];
75
+ /**
76
+ * Never safe as a generated key, in any namespace. Assigning `__proto__` in an
77
+ * object literal sets the prototype rather than defining a property, and
78
+ * `constructor` / `prototype` shadow members every object already carries.
79
+ */
80
+ exports.RESERVED_IDENTIFIERS = [
81
+ "__proto__",
82
+ "constructor",
83
+ "prototype",
84
+ ];
85
+ /**
86
+ * `id` is emitted by hand on every record type and filtered out of every input
87
+ * type, so a field that allocated it vanished from both. Reserving it means a
88
+ * field genuinely named "ID" gets a usable accessor instead.
89
+ */
90
+ exports.RESERVED_FIELD_ACCESSORS = ["id"];
91
+ /** Guards the suffix loop. Far above any real schema; see `allocate`. */
92
+ const MAX_SUFFIX_ATTEMPTS = 10_000;
93
+ /**
94
+ * Unique identifiers within one namespace.
95
+ *
96
+ * `derive` names the extra keys an allocation also occupies. A table needs it:
97
+ * its sdkName becomes both a property on `zite` and, PascalCased, a pair of
98
+ * exported type declarations, so `orders` and `Orders` are distinct accessors
99
+ * that would emit `OrdersRecordType` twice.
100
+ */
101
+ class SdkNameAllocator {
102
+ taken = new Set();
103
+ reserved;
104
+ derive;
105
+ constructor(opts = {}) {
106
+ this.reserved = new Set([
107
+ ...exports.RESERVED_IDENTIFIERS,
108
+ ...(opts.reserved ?? []),
109
+ ]);
110
+ this.derive = opts.derive ?? (() => []);
111
+ }
112
+ keysFor(name) {
113
+ return [name, ...this.derive(name)];
114
+ }
115
+ isFree(name) {
116
+ return this.keysFor(name).every((key) => !this.taken.has(key) && !this.reserved.has(key));
117
+ }
118
+ /** Free right now, without claiming it. */
119
+ available(name) {
120
+ return this.isFree(name);
121
+ }
122
+ claim(name) {
123
+ for (const key of this.keysFor(name))
124
+ this.taken.add(key);
125
+ return name;
126
+ }
127
+ /**
128
+ * `name`, or the first `name2`, `name3`… that is free. Suffixes start at 2
129
+ * because that is what the name means: the second table called Notifications.
130
+ */
131
+ allocate(preferred) {
132
+ const base = preferred || "_";
133
+ if (this.isFree(base))
134
+ return this.claim(base);
135
+ for (let suffix = 2; suffix <= MAX_SUFFIX_ATTEMPTS; suffix++) {
136
+ const candidate = `${base}${suffix}`;
137
+ if (this.isFree(candidate))
138
+ return this.claim(candidate);
139
+ }
140
+ // Unreachable: each attempt blocks at most two keys, so a free candidate
141
+ // exists long before the cap. A generator must never return a duplicate,
142
+ // so say so rather than emitting one.
143
+ throw new Error(`Could not allocate a unique SDK name for "${preferred}" after ${MAX_SUFFIX_ATTEMPTS} attempts`);
144
+ }
145
+ }
146
+ /**
147
+ * Existing sdkNames are preserved across syncs so user code keeps compiling,
148
+ * but a schema written before the sanitizer stripped trailing symbols can carry
149
+ * an invalid identifier (e.g. `timeSeconds)`). Keep one only when it is a valid
150
+ * identifier; otherwise the caller recomputes it from the display name.
151
+ */
152
+ function keepValidSdkName(sdkName) {
153
+ if (!sdkName)
154
+ return undefined;
155
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(sdkName) ? sdkName : undefined;
156
+ }
157
+ /**
158
+ * Resolve every drafted name to a unique, unreserved one.
159
+ *
160
+ * Two passes per namespace: locked names claim first in declaration order, then
161
+ * fresh ones fill in around them. Without that ordering a new table could take
162
+ * an accessor an existing app already imports.
163
+ */
164
+ function allocateSchemaNames(drafts) {
165
+ const renames = [];
166
+ const tableNames = new SdkNameAllocator({
167
+ reserved: exports.RESERVED_TABLE_ACCESSORS,
168
+ // The PascalCase form is its own pair of exported declarations.
169
+ derive: (name) => [`type:${toPascalCase(name)}`],
170
+ });
171
+ const resolved = new Map();
172
+ for (const pass of [true, false]) {
173
+ for (const entry of drafts) {
174
+ if (entry.draft.locked !== pass)
175
+ continue;
176
+ if (pass && !tableNames.available(entry.draft.preferred))
177
+ continue;
178
+ const sdkName = tableNames.allocate(entry.draft.preferred);
179
+ resolved.set(entry.table.id, sdkName);
180
+ if (sdkName !== entry.draft.preferred) {
181
+ renames.push({
182
+ kind: "table",
183
+ tableId: entry.table.id,
184
+ from: entry.draft.preferred,
185
+ to: sdkName,
186
+ });
187
+ }
188
+ }
189
+ }
190
+ // A locked name skipped in pass 1 (reserved, or claimed by an earlier locked
191
+ // name) falls through to here and is suffixed like any other.
192
+ for (const entry of drafts) {
193
+ if (resolved.has(entry.table.id))
194
+ continue;
195
+ const sdkName = tableNames.allocate(entry.draft.preferred);
196
+ resolved.set(entry.table.id, sdkName);
197
+ renames.push({
198
+ kind: "table",
199
+ tableId: entry.table.id,
200
+ from: entry.draft.preferred,
201
+ to: sdkName,
202
+ });
203
+ }
204
+ const tables = drafts.map((entry) => {
205
+ const fieldNames = new SdkNameAllocator({
206
+ reserved: exports.RESERVED_FIELD_ACCESSORS,
207
+ });
208
+ const resolvedFields = new Map();
209
+ for (const pass of [true, false]) {
210
+ for (const { field, draft } of entry.fields) {
211
+ if (draft.locked !== pass)
212
+ continue;
213
+ if (pass && !fieldNames.available(draft.preferred))
214
+ continue;
215
+ const sdkName = fieldNames.allocate(draft.preferred);
216
+ resolvedFields.set(field.id, sdkName);
217
+ if (sdkName !== draft.preferred) {
218
+ renames.push({
219
+ kind: "field",
220
+ tableId: entry.table.id,
221
+ fieldId: field.id,
222
+ from: draft.preferred,
223
+ to: sdkName,
224
+ });
225
+ }
226
+ }
227
+ }
228
+ for (const { field, draft } of entry.fields) {
229
+ if (resolvedFields.has(field.id))
230
+ continue;
231
+ const sdkName = fieldNames.allocate(draft.preferred);
232
+ resolvedFields.set(field.id, sdkName);
233
+ renames.push({
234
+ kind: "field",
235
+ tableId: entry.table.id,
236
+ fieldId: field.id,
237
+ from: draft.preferred,
238
+ to: sdkName,
239
+ });
240
+ }
241
+ return {
242
+ ...entry.table,
243
+ sdkName: resolved.get(entry.table.id),
244
+ fields: entry.fields.map(({ field }) => ({
245
+ ...field,
246
+ sdkName: resolvedFields.get(field.id),
247
+ })),
248
+ };
249
+ });
250
+ return { tables, renames };
251
+ }
252
+ /**
253
+ * Repair a schema whose names may already be unique — the idempotent case — or
254
+ * may predate allocation entirely. Everything is treated as locked: these names
255
+ * are what user code compiles against, so only a genuine conflict moves one.
256
+ */
257
+ function normalizeSchemaNames(schema) {
258
+ const { tables, renames } = allocateSchemaNames(schema.tables.map((table) => ({
259
+ table,
260
+ draft: {
261
+ preferred: keepValidSdkName(table.sdkName) ?? toSdkName(table.sdkName),
262
+ locked: true,
263
+ },
264
+ fields: table.fields.map((field) => ({
265
+ field,
266
+ draft: {
267
+ preferred: keepValidSdkName(field.sdkName) ?? toSdkName(field.sdkName),
268
+ locked: true,
269
+ },
270
+ })),
271
+ })));
272
+ return { schema: { tables }, renames };
273
+ }
274
+ /** One line per rename, for a generator that has no logger of its own. */
275
+ function describeRenames(renames) {
276
+ return renames.map((r) => r.kind === "table"
277
+ ? `table "${r.from}" -> "${r.to}" (id ${r.tableId})`
278
+ : `field "${r.from}" -> "${r.to}" (table ${r.tableId}, field ${r.fieldId})`);
279
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,205 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const lib_js_1 = require("./lib.js");
5
+ const sdkNames_js_1 = require("./sdkNames.js");
6
+ // `generateSchema` warns on every rename; silence it so the suite stays
7
+ // readable, and assert on the emitted names instead.
8
+ (0, vitest_1.beforeEach)(() => {
9
+ vitest_1.vi.spyOn(console, "warn").mockImplementation(() => { });
10
+ });
11
+ (0, vitest_1.afterEach)(() => {
12
+ vitest_1.vi.restoreAllMocks();
13
+ });
14
+ const field = (id, name) => ({ id, name, type: "single_line_text", order: 0 });
15
+ const database = (tables) => ({
16
+ id: "base_1",
17
+ name: "Base",
18
+ tables,
19
+ createdAt: "",
20
+ updatedAt: "",
21
+ url: "",
22
+ });
23
+ /** Every key in the emitted `export const zite = { … }` literal. */
24
+ const ziteKeys = (source) => {
25
+ const body = source.slice(source.indexOf("export const zite = {"));
26
+ return [...body.matchAll(/^ {2}([A-Za-z_$][\w$]*):/gm)].map((m) => m[1]);
27
+ };
28
+ /** Every property key on one emitted record type. */
29
+ const recordTypeKeys = (source, typeName) => {
30
+ const start = source.indexOf(`export type ${typeName} = {`);
31
+ const body = source.slice(start, source.indexOf("};", start));
32
+ return [...body.matchAll(/^ {2}([A-Za-z_$][\w$]*)\??:/gm)].map((m) => m[1]);
33
+ };
34
+ (0, vitest_1.describe)("reserved platform accessors", () => {
35
+ // Sudzy Dashboard: a table named "Notifications" emitted a second
36
+ // `notifications:` key, and the platform client — written last — won. The
37
+ // table lost findAll and typed as `{ create(NotificationsCreateParams) }`.
38
+ (0, vitest_1.it)("moves a table off the notifications accessor", () => {
39
+ const schema = (0, lib_js_1.generateSchema)(database([{ id: "tbl_1", name: "Notifications", fields: [] }]));
40
+ (0, vitest_1.expect)(schema.tables[0].sdkName).toBe("notifications2");
41
+ const keys = ziteKeys((0, lib_js_1.generateDbTs)(schema));
42
+ (0, vitest_1.expect)(keys).toContain("notifications2");
43
+ (0, vitest_1.expect)(keys.filter((k) => k === "notifications")).toHaveLength(1);
44
+ (0, vitest_1.expect)(new Set(keys).size).toBe(keys.length);
45
+ });
46
+ vitest_1.it.each(["sql", "auth", "notifications"])("keeps the platform %s accessor for the platform", (reserved) => {
47
+ const schema = (0, lib_js_1.generateSchema)(database([{ id: "tbl_1", name: reserved, fields: [] }]));
48
+ (0, vitest_1.expect)(schema.tables[0].sdkName).toBe(`${reserved}2`);
49
+ const keys = ziteKeys((0, lib_js_1.generateDbTs)(schema));
50
+ (0, vitest_1.expect)(new Set(keys).size).toBe(keys.length);
51
+ });
52
+ vitest_1.it.each(["__proto__", "constructor", "prototype"])("refuses %s as a table accessor", (hazard) => {
53
+ const schema = (0, lib_js_1.generateSchema)(database([{ id: "tbl_1", name: hazard, fields: [] }]));
54
+ (0, vitest_1.expect)(schema.tables[0].sdkName).not.toBe(hazard);
55
+ });
56
+ });
57
+ (0, vitest_1.describe)("collision de-duplication", () => {
58
+ // Nevada Car Coalition: 1.0's generator suffixed colliding names, so the app
59
+ // was written against `category1` / `sponsor1`. 2.0 had no accumulator at
60
+ // all, so the second field silently overwrote the first.
61
+ (0, vitest_1.it)("keeps both fields when two display names normalize alike", () => {
62
+ const schema = (0, lib_js_1.generateSchema)(database([
63
+ {
64
+ id: "tbl_1",
65
+ name: "K9 Winners",
66
+ fields: [field("fld_1", "Category"), field("fld_2", "category")],
67
+ },
68
+ ]));
69
+ const names = schema.tables[0].fields.map((f) => f.sdkName);
70
+ (0, vitest_1.expect)(names).toEqual(["category", "category2"]);
71
+ const keys = recordTypeKeys((0, lib_js_1.generateDbTs)(schema), "K9WinnersRecordType");
72
+ (0, vitest_1.expect)(keys).toEqual(["id", "category", "category2"]);
73
+ });
74
+ (0, vitest_1.it)("de-duplicates two tables that normalize alike", () => {
75
+ const schema = (0, lib_js_1.generateSchema)(database([
76
+ { id: "tbl_1", name: "Orders", fields: [] },
77
+ { id: "tbl_2", name: "orders", fields: [] },
78
+ ]));
79
+ (0, vitest_1.expect)(schema.tables.map((t) => t.sdkName)).toEqual(["orders", "orders2"]);
80
+ });
81
+ // Two sdkNames sharing a PascalCase form emit `FooRecordType` twice, which is
82
+ // a duplicate identifier even though the accessors differ.
83
+ (0, vitest_1.it)("separates tables whose class names would collide", () => {
84
+ const source = (0, lib_js_1.generateDbTs)({
85
+ tables: [
86
+ { id: "tbl_1", sdkName: "orders", fields: [] },
87
+ { id: "tbl_2", sdkName: "Orders", fields: [] },
88
+ ],
89
+ });
90
+ const declared = [...source.matchAll(/export type (\w+RecordType)/g)].map((m) => m[1]);
91
+ (0, vitest_1.expect)(new Set(declared).size).toBe(declared.length);
92
+ });
93
+ (0, vitest_1.it)("reserves id so a field named ID stays readable", () => {
94
+ const schema = (0, lib_js_1.generateSchema)(database([{ id: "tbl_1", name: "Rows", fields: [field("fld_1", "ID")] }]));
95
+ (0, vitest_1.expect)(schema.tables[0].fields[0].sdkName).toBe("id2");
96
+ (0, vitest_1.expect)(recordTypeKeys((0, lib_js_1.generateDbTs)(schema), "RowsRecordType")).toEqual([
97
+ "id",
98
+ "id2",
99
+ ]);
100
+ });
101
+ });
102
+ (0, vitest_1.describe)("name stability", () => {
103
+ const existing = {
104
+ tables: [
105
+ {
106
+ id: "tbl_1",
107
+ sdkName: "priceList",
108
+ fields: [
109
+ { id: "fld_1", sdkName: "srp", definition: { type: "number" } },
110
+ ],
111
+ },
112
+ ],
113
+ };
114
+ // Kelly: the app was written against `srp`, and the field's display name has
115
+ // drifted since. A locked name is what the app compiles against.
116
+ (0, vitest_1.it)("preserves a locked name over the one the display name would produce", () => {
117
+ const schema = (0, lib_js_1.generateSchema)(database([
118
+ {
119
+ id: "tbl_1",
120
+ name: "Price List",
121
+ fields: [field("fld_1", "SRP (Suggested Retail Price)")],
122
+ },
123
+ ]), existing);
124
+ (0, vitest_1.expect)(schema.tables[0].fields[0].sdkName).toBe("srp");
125
+ });
126
+ (0, vitest_1.it)("does not let a new field displace a locked one", () => {
127
+ const schema = (0, lib_js_1.generateSchema)(database([
128
+ {
129
+ id: "tbl_1",
130
+ name: "Price List",
131
+ // The new field's fresh name is exactly what fld_1 holds.
132
+ fields: [field("fld_2", "SRP"), field("fld_1", "Anything")],
133
+ },
134
+ ]), existing);
135
+ const byId = Object.fromEntries(schema.tables[0].fields.map((f) => [f.id, f.sdkName]));
136
+ (0, vitest_1.expect)(byId.fld_1).toBe("srp");
137
+ (0, vitest_1.expect)(byId.fld_2).toBe("srp2");
138
+ });
139
+ (0, vitest_1.it)("is stable across repeated generation", () => {
140
+ const db = database([
141
+ {
142
+ id: "tbl_1",
143
+ name: "Notifications",
144
+ fields: [field("fld_1", "Category"), field("fld_2", "category")],
145
+ },
146
+ ]);
147
+ const first = (0, lib_js_1.generateSchema)(db);
148
+ const second = (0, lib_js_1.generateSchema)(db, first);
149
+ const third = (0, lib_js_1.generateSchema)(db, second);
150
+ (0, vitest_1.expect)(second).toEqual(first);
151
+ (0, vitest_1.expect)(third).toEqual(first);
152
+ });
153
+ (0, vitest_1.it)("recomputes a name that is not a valid identifier", () => {
154
+ const schema = (0, lib_js_1.generateSchema)(database([
155
+ { id: "tbl_1", name: "Rows", fields: [field("fld_1", "Time")] },
156
+ ]), {
157
+ tables: [
158
+ {
159
+ id: "tbl_1",
160
+ sdkName: "rows",
161
+ fields: [
162
+ {
163
+ id: "fld_1",
164
+ sdkName: "timeSeconds)",
165
+ definition: { type: "number" },
166
+ },
167
+ ],
168
+ },
169
+ ],
170
+ });
171
+ (0, vitest_1.expect)(schema.tables[0].fields[0].sdkName).toBe("time");
172
+ });
173
+ });
174
+ (0, vitest_1.describe)("normalizeSchemaNames", () => {
175
+ (0, vitest_1.it)("is a no-op on a well-formed schema", () => {
176
+ const schema = {
177
+ tables: [
178
+ {
179
+ id: "tbl_1",
180
+ sdkName: "orders",
181
+ fields: [
182
+ { id: "fld_1", sdkName: "total", definition: { type: "number" } },
183
+ ],
184
+ },
185
+ ],
186
+ };
187
+ const result = (0, sdkNames_js_1.normalizeSchemaNames)(schema);
188
+ (0, vitest_1.expect)(result.renames).toEqual([]);
189
+ (0, vitest_1.expect)(result.schema).toEqual(schema);
190
+ });
191
+ // A schema committed before allocation existed. The sandbox's boot-time
192
+ // `zitejs generate` reads it with no generateSchema pass in front, so this is
193
+ // the only thing standing between it and a TS1117 it cannot build past.
194
+ (0, vitest_1.it)("repairs a committed schema that already holds the collision", () => {
195
+ const source = (0, lib_js_1.generateDbTs)({
196
+ tables: [
197
+ { id: "tbl_1", sdkName: "notifications", fields: [] },
198
+ { id: "tbl_2", sdkName: "orders", fields: [] },
199
+ ],
200
+ });
201
+ const keys = ziteKeys(source);
202
+ (0, vitest_1.expect)(new Set(keys).size).toBe(keys.length);
203
+ (0, vitest_1.expect)(keys).toContain("notifications2");
204
+ });
205
+ });
@@ -1,5 +1,6 @@
1
1
  import { createAuthClient } from 'better-auth/react';
2
2
  import { magicLinkClient, inferAdditionalFields, } from 'better-auth/client/plugins';
3
+ import { USAGE_TOKEN_QUERY_PARAM } from './constants.js';
3
4
  const authClient = createAuthClient({
4
5
  baseURL: '',
5
6
  plugins: [
@@ -50,10 +51,21 @@ export function loginWithRedirect(opts) {
50
51
  // the address bar of every logged-out visitor to the front page, which is the
51
52
  // common case. Only pass a destination when it IS one.
52
53
  const isRoot = target.pathname === '/' && target.search === '' && target.hash === '';
53
- window.location.href = isRoot
54
- ? '/auth/login'
55
- : '/auth/login?' +
56
- new URLSearchParams({ redirectUrl: target.toString() }).toString();
54
+ const params = new URLSearchParams();
55
+ if (!isRoot)
56
+ params.set('redirectUrl', target.toString());
57
+ // The editor preview rides its usageToken on the app URL; the sign-in page
58
+ // only shows its editor surface ("Preview as" picker) when that token reaches
59
+ // it. Capturing the current URL carries it implicitly — but an explicit
60
+ // `redirectUrl` names a bare path, and would silently drop it. Forward it
61
+ // top-level either way, so how the app phrases its redirect can't decide
62
+ // whether the editor gets a preview. Published visitors never have one.
63
+ const usageToken = new URL(window.location.href).searchParams.get(USAGE_TOKEN_QUERY_PARAM) ??
64
+ window._ziteUsageToken;
65
+ if (usageToken)
66
+ params.set(USAGE_TOKEN_QUERY_PARAM, usageToken);
67
+ const query = params.toString();
68
+ window.location.href = query ? `/auth/login?${query}` : '/auth/login';
57
69
  }
58
70
  export function logout(opts) {
59
71
  signOut().then(() => {
@@ -112,4 +112,43 @@ describe('loginWithRedirect', () => {
112
112
  expect(params.get('view')).toBeNull();
113
113
  expect(params.get('redirectUrl')).toBe('https://app.zite.so/pricing');
114
114
  });
115
+ it('forwards the editor usageToken past an explicit redirectUrl', () => {
116
+ // The editor preview rides its token on the app URL. An explicit
117
+ // redirectUrl names a bare path, which used to silently drop it — and with
118
+ // it the sign-in page's whole editor surface ("Preview as" picker).
119
+ const location = stubLocation('https://app.zite.so/?usageToken=tok-123');
120
+ authExports.loginWithRedirect({ redirectUrl: '/dashboard' });
121
+ const params = new URLSearchParams(location.href.split('?')[1] ?? '');
122
+ expect(params.get('usageToken')).toBe('tok-123');
123
+ expect(params.get('redirectUrl')).toBe('https://app.zite.so/dashboard');
124
+ });
125
+ it('forwards the usageToken top-level on a bare call too', () => {
126
+ const location = stubLocation('https://app.zite.so/orders/123?usageToken=tok-123');
127
+ authExports.loginWithRedirect();
128
+ const params = new URLSearchParams(location.href.split('?')[1] ?? '');
129
+ expect(params.get('usageToken')).toBe('tok-123');
130
+ });
131
+ it('carries a token to a root destination that would otherwise say nothing', () => {
132
+ const location = stubLocation('https://app.zite.so/?usageToken=tok-123');
133
+ // The token is the whole search string here only if nothing else rides
134
+ // along — an explicit root override drops the query, exercising the
135
+ // token-only branch.
136
+ authExports.loginWithRedirect({ redirectUrl: '/' });
137
+ expect(location.href).toBe('/auth/login?usageToken=tok-123');
138
+ });
139
+ it('falls back to window._ziteUsageToken when the URL was scrubbed', () => {
140
+ // app-runner's injected boot script moves the token off the address bar
141
+ // into this global before the bundle runs.
142
+ const location = stubLocation('https://app.zite.so/pricing');
143
+ window._ziteUsageToken = 'tok-456';
144
+ authExports.loginWithRedirect({ redirectUrl: '/dashboard' });
145
+ const params = new URLSearchParams(location.href.split('?')[1] ?? '');
146
+ expect(params.get('usageToken')).toBe('tok-456');
147
+ });
148
+ it('adds no token param for ordinary visitors', () => {
149
+ const location = stubLocation('https://app.zite.so/pricing');
150
+ authExports.loginWithRedirect();
151
+ const params = new URLSearchParams(location.href.split('?')[1] ?? '');
152
+ expect(params.get('usageToken')).toBeNull();
153
+ });
115
154
  });
@@ -14,18 +14,7 @@ export type ZiteSchemaTable = {
14
14
  export type ZiteSchema = {
15
15
  tables: ZiteSchemaTable[];
16
16
  };
17
- export declare function toPascalCase(name: string): string;
18
- export declare function toCamelCase(name: string): string;
19
- /**
20
- * camelCase for a FRESH sdkName, reading acronym runs as words: "VIP" ->
21
- * "vip", "APIKey" -> "apiKey" (plain toCamelCase yields "vIP" / "aPIKey").
22
- *
23
- * Deliberately not folded into toCamelCase: that also derives endpoint
24
- * identifiers from existing filenames on every generate, where normalizing
25
- * would rename a working app's `api.sendSMS`. This is only for names being
26
- * chosen for the first time — generateSchema preserves existing sdkNames.
27
- */
28
- export declare function toSdkName(name: string): string;
17
+ export { toPascalCase, toCamelCase, toSdkName } from "./sdkNames.js";
29
18
  /**
30
19
  * Build a ZiteSchema from a Database API response.
31
20
  *
@@ -39,7 +28,7 @@ export declare function generateSchema(database: Database, existingSchema?: Zite
39
28
  * Generate .zite/db.ts purely from ZiteSchema.
40
29
  * Pure function — no external data needed beyond what's in the schema file.
41
30
  */
42
- export declare function generateDbTs(schema: ZiteSchema): string;
31
+ export declare function generateDbTs(inputSchema: ZiteSchema): string;
43
32
  export type EndpointFileInfo = {
44
33
  fileName: string;
45
34
  content?: string;