@teleporthq/teleport-shared 0.44.0 → 0.44.1

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.
Files changed (41) hide show
  1. package/__tests__/utils/email-sender.ts +123 -0
  2. package/__tests__/utils/table-access-page-reads.ts +183 -0
  3. package/dist/cjs/index.d.ts +2 -1
  4. package/dist/cjs/index.d.ts.map +1 -1
  5. package/dist/cjs/index.js +3 -1
  6. package/dist/cjs/index.js.map +1 -1
  7. package/dist/cjs/page-transition/view-transition-css.d.ts +4 -0
  8. package/dist/cjs/page-transition/view-transition-css.d.ts.map +1 -1
  9. package/dist/cjs/page-transition/view-transition-css.js +17 -10
  10. package/dist/cjs/page-transition/view-transition-css.js.map +1 -1
  11. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  12. package/dist/cjs/utils/email-sender.d.ts +43 -0
  13. package/dist/cjs/utils/email-sender.d.ts.map +1 -0
  14. package/dist/cjs/utils/email-sender.js +36 -0
  15. package/dist/cjs/utils/email-sender.js.map +1 -0
  16. package/dist/cjs/utils/table-access.d.ts +32 -0
  17. package/dist/cjs/utils/table-access.d.ts.map +1 -1
  18. package/dist/cjs/utils/table-access.js +118 -2
  19. package/dist/cjs/utils/table-access.js.map +1 -1
  20. package/dist/esm/index.d.ts +2 -1
  21. package/dist/esm/index.d.ts.map +1 -1
  22. package/dist/esm/index.js +2 -1
  23. package/dist/esm/index.js.map +1 -1
  24. package/dist/esm/page-transition/view-transition-css.d.ts +4 -0
  25. package/dist/esm/page-transition/view-transition-css.d.ts.map +1 -1
  26. package/dist/esm/page-transition/view-transition-css.js +15 -9
  27. package/dist/esm/page-transition/view-transition-css.js.map +1 -1
  28. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  29. package/dist/esm/utils/email-sender.d.ts +43 -0
  30. package/dist/esm/utils/email-sender.d.ts.map +1 -0
  31. package/dist/esm/utils/email-sender.js +32 -0
  32. package/dist/esm/utils/email-sender.js.map +1 -0
  33. package/dist/esm/utils/table-access.d.ts +32 -0
  34. package/dist/esm/utils/table-access.d.ts.map +1 -1
  35. package/dist/esm/utils/table-access.js +115 -1
  36. package/dist/esm/utils/table-access.js.map +1 -1
  37. package/package.json +2 -2
  38. package/src/index.ts +2 -0
  39. package/src/page-transition/view-transition-css.ts +27 -16
  40. package/src/utils/email-sender.ts +74 -0
  41. package/src/utils/table-access.ts +148 -1
@@ -232,6 +232,120 @@ export var resolveAdminPanelRoles = function (auth) {
232
232
  export var resolveTrustedReaderRoles = function (auth) {
233
233
  return resolveAdminPanelRoles(auth);
234
234
  };
235
+ /**
236
+ * What a page shows of a member to whoever opens it: their public profile —
237
+ * who they are, never how to reach them. A page's own data, read for a visitor,
238
+ * carries only these columns of the auth users table: never an email, a phone
239
+ * or an address, a credential, a role or anything else of the account. The
240
+ * member's own private fields reach their own page from their session.
241
+ */
242
+ export var PUBLIC_PROFILE_COLUMNS = [
243
+ 'id',
244
+ 'name',
245
+ 'display_name',
246
+ 'image',
247
+ 'avatar',
248
+ 'avatar_url',
249
+ 'bio',
250
+ ];
251
+ var bareTableName = function (tableName) {
252
+ var parts = String(tableName || '')
253
+ .trim()
254
+ .toLowerCase()
255
+ .split('.');
256
+ return parts[parts.length - 1].replace(/"/g, '');
257
+ };
258
+ /**
259
+ * The tables a page's own data, read for a visitor (`pageReadsAsStore` false),
260
+ * gets no row of — so it need not ask: the protected ones, and the feed tables,
261
+ * whose one visitor projection lives on the data API. Not the auth users table:
262
+ * a page reads its members' public profiles (`PUBLIC_PROFILE_COLUMNS`).
263
+ */
264
+ export var pageDataReadsNoRowOf = function (tableName) {
265
+ var bare = bareTableName(tableName);
266
+ if (AUTH_CREDENTIAL_TABLES.indexOf(bare) !== -1) {
267
+ return false;
268
+ }
269
+ return (PROTECTED_TABLES.indexOf(bare) !== -1 ||
270
+ Object.prototype.hasOwnProperty.call(PUBLIC_FEED_TABLES, bare));
271
+ };
272
+ var isAdminRoute = function (route) {
273
+ return route === ADMIN_ROUTE_PREFIX || route.startsWith("".concat(ADMIN_ROUTE_PREFIX, "/"));
274
+ };
275
+ /**
276
+ * The gate the generated middleware puts on each route it knows
277
+ * (`generateMiddlewareFile`): a page's own protection, the roles of the
278
+ * role-gated folders it sits in merged in, and `/admin` — when any admin page
279
+ * is protected — admitting the admin panel's roles. A row-owned page is left
280
+ * to its own SQL there, so it gates nothing here either.
281
+ */
282
+ var middlewareRouteGates = function (auth) {
283
+ var gates = new Map();
284
+ var folders = Object.values(auth.folderProtection || {});
285
+ Object.entries(auth.pageProtection || {}).forEach(function (_a) {
286
+ var pageId = _a[0], page = _a[1];
287
+ if (!page || typeof page.route !== 'string' || page.rowOwnerColumn) {
288
+ return;
289
+ }
290
+ var folderRoles = folders
291
+ .filter(function (folder) { var _a; return ((_a = folder === null || folder === void 0 ? void 0 : folder.children) === null || _a === void 0 ? void 0 : _a[pageId]) === 'page'; })
292
+ .flatMap(function (folder) { return folder.allowedRoles || []; });
293
+ var existing = gates.get(page.route);
294
+ gates.set(page.route, {
295
+ requiresAuth: !!(existing === null || existing === void 0 ? void 0 : existing.requiresAuth) || !!page.requiresAuth,
296
+ roles: Array.from(new Set(__spreadArray(__spreadArray(__spreadArray([], ((existing === null || existing === void 0 ? void 0 : existing.roles) || []), true), (page.allowedRoles || []), true), folderRoles, true))),
297
+ });
298
+ });
299
+ if (!gates.has(ADMIN_ROUTE_PREFIX) && Array.from(gates.keys()).some(isAdminRoute)) {
300
+ var panelRoles = resolveAdminPanelRoles(auth);
301
+ gates.set(ADMIN_ROUTE_PREFIX, {
302
+ requiresAuth: true,
303
+ roles: panelRoles.length > 0 ? panelRoles : ['admin'],
304
+ });
305
+ }
306
+ return gates;
307
+ };
308
+ /**
309
+ * Whether a page's own data — what its getStaticProps / getServerSideProps
310
+ * read to render it — may be read as the store rather than as a visitor.
311
+ *
312
+ * Only an admin panel page that nobody but the store's trusted readers may
313
+ * open: at `/admin` or below, behind the middleware, with every role its gate
314
+ * admits in `resolveTrustedReaderRoles`. Whoever receives that page's data
315
+ * could read the same rows through the data routes. Every other page — public,
316
+ * members-only, or an admin screen another role may open — is rendered with
317
+ * what a visitor is served, since its data reaches whoever opens it, and
318
+ * through `/_next/data` without rendering it at all. `route` is the page's
319
+ * path when its own protection does not name it.
320
+ */
321
+ export var pageReadsAsStore = function (page, auth) {
322
+ var _a;
323
+ if (!auth) {
324
+ return false;
325
+ }
326
+ var trustedRoles = resolveTrustedReaderRoles(auth);
327
+ if (trustedRoles.length === 0) {
328
+ return false;
329
+ }
330
+ var own = page.pageId ? (_a = auth.pageProtection) === null || _a === void 0 ? void 0 : _a[page.pageId] : undefined;
331
+ if (own === null || own === void 0 ? void 0 : own.rowOwnerColumn) {
332
+ return false;
333
+ }
334
+ var route = typeof (own === null || own === void 0 ? void 0 : own.route) === 'string' ? own.route : page.route;
335
+ if (!route || !isAdminRoute(route)) {
336
+ return false;
337
+ }
338
+ // The middleware's resolution: the route's own gate, else its nearest gated ancestor.
339
+ var gates = middlewareRouteGates(auth);
340
+ var ancestor = Array.from(gates.keys())
341
+ .filter(function (gated) { return route.startsWith("".concat(gated, "/")); })
342
+ .sort(function (a, b) { return b.length - a.length; })[0];
343
+ var gate = gates.get(route) || (ancestor ? gates.get(ancestor) : undefined);
344
+ if (!gate || !gate.requiresAuth || gate.roles.length === 0) {
345
+ return false;
346
+ }
347
+ return gate.roles.every(function (role) { return trustedRoles.indexOf(role) !== -1; });
348
+ };
235
349
  /**
236
350
  * ES5 source of the guard, inlined into a generated route. Declares:
237
351
  * - `__taIsInternalRequest(req)` — the caller presented the app secret, or
@@ -266,6 +380,6 @@ export var resolveTrustedReaderRoles = function (auth) {
266
380
  * `__tqSessionToken` resolver, which the host route must inline as well.
267
381
  */
268
382
  export var generateTableAccessHelperCode = function (params) {
269
- return "// GENERATED \u2014 see generateTableAccessHelperCode in\n// @teleporthq/teleport-shared/src/utils/table-access.ts.\n//\n// Tables that ARE money, and tables that decide how much of it is given away.\n// A browser reads the first group only as a trusted role (the generated admin)\n// and writes the second group never; server-side workflow segments present the\n// app secret instead. One list for both data-route families.\nvar __TA_PROTECTED_TABLES = ".concat(JSON.stringify(PROTECTED_TABLES), ";\nvar __TA_WRITE_PROTECTED_TABLES = ").concat(JSON.stringify(WRITE_PROTECTED_TABLES), ";\n// Readable from a browser only as this projection of the rows the database\n// calls live \u2014 see PUBLIC_FEED_TABLES. The per-table routes serve a whole\n// table (and raw SQL), which cannot express a feed, so there they are refused\n// like a protected table.\nvar __TA_PUBLIC_FEED_TABLES = ").concat(JSON.stringify(PUBLIC_FEED_TABLES), ";\nvar __TA_FEED_TABLES = Object.keys(__TA_PUBLIC_FEED_TABLES);\nvar __TA_TRUSTED_READER_ROLES = ").concat(JSON.stringify(params.trustedReaderRoles), ";\nvar __TA_CREDENTIAL_COLUMNS = ").concat(JSON.stringify(AUTH_CREDENTIAL_COLUMNS), ";\nvar __TA_UNAMBIGUOUS_CREDENTIAL_COLUMNS = ").concat(JSON.stringify(UNAMBIGUOUS_CREDENTIAL_COLUMNS), ";\nvar __TA_CREDENTIAL_TABLES = ").concat(JSON.stringify(AUTH_CREDENTIAL_TABLES), ";\nvar __TA_SQL_WRITE_RE = /\\b(?:insert|update|delete|merge|truncate)\\b/i;\n// What a statement can reach without spelling a table's name where this scan\n// can see it: a Unicode-escaped identifier (U&\"\\0075sers\" is `users`), the\n// functions that run SQL text or dump whole tables, schemas or the database\n// (the table name then sits in a string, or is not named at all), and the\n// planner statistics, which hold sampled column values of every table.\nvar __TA_OPAQUE_SQL_RE = /u&\"|\\b(?:query|table|cursor|schema|database)_to_xml\\w*\\s*\\(|\\bts_(?:stat|rewrite)\\s*\\(|\\bdblink\\w*\\s*\\(|\\bpg_read_(?:binary_)?file\\s*\\(|\\blo_(?:import|export|get)\\s*\\(|\\bpg_stat(?:s|istic)\\w*/i;\n\n// Only server code can read NEXTAUTH_SECRET, so a browser cannot forge the\n// header a server segment sends. An in-process call (getStaticProps and the\n// page-load workflows call the handler directly with a synthetic request)\n// marks its request object instead \u2014 a property, never a header, so nothing\n// on the wire can claim it.\nfunction __taPresentsAppSecret(req) {\n var secret = req && req.headers && req.headers['x-internal-data-secret'];\n return !!(secret && process.env.NEXTAUTH_SECRET && secret === process.env.NEXTAUTH_SECRET);\n}\n\nfunction __taIsRestrictedRequest(req) {\n return __taPresentsAppSecret(req) && !!(req.headers && req.headers['x-tq-restricted-sql'] === '1');\n}\n\nfunction __taIsInternalRequest(req) {\n if (req && req.__tqServerCall === true) return true;\n return __taPresentsAppSecret(req) && !__taIsRestrictedRequest(req);\n}\n\n// Generated browser code reads through the structured routes and never sends\n// SQL or a write of its own \u2014 every data node runs server-side and presents\n// the secret. So where a secret exists to tell the two apart, anything else a\n// browser sends is refused whole: no statement scan has to be right about it.\n// An app without one (no authentication, hence no store) keeps the scan.\nfunction __taBrowserIsReadOnly(req) {\n return !!process.env.NEXTAUTH_SECRET && !__taIsInternalRequest(req) && !__taIsRestrictedRequest(req);\n}\n\n// Comments and string literals out, so a table name inside a string neither\n// hides a reference nor fakes one. Quoted identifiers stay, whole: in Postgres\n// they name tables, which is exactly what is being looked for.\n//\n// \u26D4 This is a lexer, not a search: a quote or comment marker inside a quoted\n// identifier (\"x'\", \"--\") is part of the name, a `$` or an `E` inside an\n// identifier (a$b$, ELSE'\u2026') starts neither a dollar quote nor an escape\n// string, and a dollar-quoted body is consumed by its own tag. An UNTERMINATED\n// literal, identifier, comment or dollar quote makes the whole scan fail closed\n// (ok:false): a statement this cleaner cannot read is one it cannot vouch for.\nfunction __taCleanSql(sql) {\n var text = String(sql || '');\n var out = '';\n var i = 0;\n function inIdentifier(at) { return at > 0 && /[A-Za-z0-9_$]/.test(text[at - 1]); }\n while (i < text.length) {\n var ch = text[i];\n var next = text[i + 1];\n if (ch === '-' && next === '-') {\n while (i < text.length && text[i] !== '\\n') i++;\n continue;\n }\n if (ch === '/' && next === '*') {\n i += 2;\n var depth = 1;\n while (i < text.length && depth > 0) {\n if (text[i] === '/' && text[i + 1] === '*') { depth++; i += 2; continue; }\n if (text[i] === '*' && text[i + 1] === '/') { depth--; i += 2; continue; }\n i++;\n }\n if (depth > 0) return { ok: false, text: out };\n out += ' ';\n continue;\n }\n // \"\u2026\" is one identifier token; \"\" inside it is an escaped quote.\n if (ch === '\"') {\n var end = i + 1;\n var closedIdentifier = false;\n while (end < text.length) {\n if (text[end] === '\"' && text[end + 1] === '\"') { end += 2; continue; }\n if (text[end] === '\"') { closedIdentifier = true; break; }\n end++;\n }\n if (!closedIdentifier) return { ok: false, text: out };\n out += text.slice(i, end + 1);\n i = end + 1;\n continue;\n }\n // $tag$ \u2026 $tag$ \u2014 the body is data whatever it holds.\n if (ch === '$' && !inIdentifier(i)) {\n var tagMatch = /^\\$[A-Za-z_][A-Za-z0-9_]*\\$|^\\$\\$/.exec(text.slice(i));\n if (tagMatch) {\n var tag = tagMatch[0];\n var close = text.indexOf(tag, i + tag.length);\n if (close === -1) return { ok: false, text: out };\n i = close + tag.length;\n out += ' ';\n continue;\n }\n }\n // E'\u2026' understands backslash escapes; a plain '\u2026' doubles the quote.\n if (ch === \"'\" || ((ch === 'E' || ch === 'e') && next === \"'\" && !inIdentifier(i))) {\n var escapes = ch !== \"'\";\n if (escapes) i++;\n i++;\n var closed = false;\n while (i < text.length) {\n if (escapes && text[i] === '\\\\') { i += 2; continue; }\n if (text[i] === \"'\" && text[i + 1] === \"'\") { i += 2; continue; }\n if (text[i] === \"'\") { i++; closed = true; break; }\n i++;\n }\n if (!closed) return { ok: false, text: out };\n out += ' ';\n continue;\n }\n out += ch;\n i++;\n }\n return { ok: true, text: out };\n}\n\nfunction __taNormalizeTable(tableName) {\n var name = String(tableName || '').trim().toLowerCase();\n var parts = name.split('.');\n return parts[parts.length - 1].replace(/\"/g, '');\n}\n\n// An unreadable statement, or one that can reach a table without naming it,\n// counts as mentioning EVERY protected table: fail closed rather than let a\n// spelling this scan cannot see decide it is harmless.\nfunction __taSqlMentionsTable(sql, table) {\n var cleaned = __taCleanSql(sql);\n if (!cleaned.ok || __TA_OPAQUE_SQL_RE.test(cleaned.text)) return true;\n var pattern = new RegExp('(?:^|[^A-Za-z0-9_])\"?' + table + '\"?(?![A-Za-z0-9_])', 'i');\n return pattern.test(cleaned.text);\n}\n\nfunction __taIsCredentialTable(tableName) {\n return __TA_CREDENTIAL_TABLES.indexOf(__taNormalizeTable(tableName)) !== -1;\n}\n\n// Credentials never travel in a row: a column is dropped by name, however the\n// caller spelled it in its projection.\nfunction __taWithoutCredentials(rows, tableName) {\n if (!Array.isArray(rows)) return rows;\n var dropped = tableName && __taIsCredentialTable(tableName)\n ? __TA_CREDENTIAL_COLUMNS\n : tableName ? [] : __TA_UNAMBIGUOUS_CREDENTIAL_COLUMNS;\n if (dropped.length === 0) return rows;\n return rows.map(function(row) {\n if (!row || typeof row !== 'object') return row;\n var copy = {};\n Object.keys(row).forEach(function(key) {\n if (dropped.indexOf(String(key).toLowerCase()) === -1) copy[key] = row[key];\n });\n return copy;\n });\n}\n\nfunction __taForbid(table) {\n var err = new Error('Forbidden: ' + table + ' is only accessible to server-side workflow nodes');\n err.status = 403;\n throw err;\n}\n\nfunction __taAssertSqlTextAllowed(sql) {\n var text = String(sql || '');\n // A statement can hand back a whole row (`SELECT u FROM users u`,\n // `row_to_json(u)`) without naming a credential column, so the table itself\n // is out of reach of anything but the server's own calls.\n for (var ci = 0; ci < __TA_CREDENTIAL_TABLES.length; ci++) {\n if (__taSqlMentionsTable(text, __TA_CREDENTIAL_TABLES[ci])) __taForbid(__TA_CREDENTIAL_TABLES[ci]);\n }\n for (var pi = 0; pi < __TA_PROTECTED_TABLES.length; pi++) {\n if (__taSqlMentionsTable(text, __TA_PROTECTED_TABLES[pi])) __taForbid(__TA_PROTECTED_TABLES[pi]);\n }\n // A feed table has exactly one browser-readable shape, and a statement the\n // caller wrote is not it.\n for (var fi = 0; fi < __TA_FEED_TABLES.length; fi++) {\n if (__taSqlMentionsTable(text, __TA_FEED_TABLES[fi])) __taForbid(__TA_FEED_TABLES[fi]);\n }\n var cleaned = __taCleanSql(text);\n // Unreadable, or a write: either way the write-protected tables are refused.\n if (cleaned.ok && !__TA_SQL_WRITE_RE.test(cleaned.text)) return;\n for (var wi = 0; wi < __TA_WRITE_PROTECTED_TABLES.length; wi++) {\n if (__taSqlMentionsTable(text, __TA_WRITE_PROTECTED_TABLES[wi])) __taForbid(__TA_WRITE_PROTECTED_TABLES[wi]);\n }\n}\n\nfunction __taRoleOf(token) {\n if (!token || typeof token !== 'object') return null;\n if (typeof token.role === 'string') return token.role;\n if (typeof token.roleName === 'string') return token.roleName;\n if (Array.isArray(token.roles) && typeof token.roles[0] === 'string') return token.roles[0];\n return null;\n}\n\n// The tables a read of `tableName` (or the raw statement) touches that this\n// route family may not serve to a browser: the protected ones, and the feed\n// tables, whose one browser-readable projection lives on the data API instead.\nvar __TA_ROUTE_PROTECTED_TABLES = __TA_PROTECTED_TABLES.concat(__TA_FEED_TABLES);\n\nfunction __taProtectedTablesRead(tableName, rawQuery) {\n var touched = [];\n var bare = __taNormalizeTable(tableName);\n if (__TA_ROUTE_PROTECTED_TABLES.indexOf(bare) !== -1) touched.push(bare);\n if (typeof rawQuery === 'string' && rawQuery.length > 0) {\n for (var i = 0; i < __TA_ROUTE_PROTECTED_TABLES.length; i++) {\n if (touched.indexOf(__TA_ROUTE_PROTECTED_TABLES[i]) === -1 && __taSqlMentionsTable(rawQuery, __TA_ROUTE_PROTECTED_TABLES[i])) {\n touched.push(__TA_ROUTE_PROTECTED_TABLES[i]);\n }\n }\n }\n return touched;\n}\n\n// The projection an untrusted caller gets of a feed table, or null when the\n// table has no feed or the caller is a server-side one reading it whole.\nfunction __taPublicFeed(req, tableName) {\n if (__taIsInternalRequest(req)) return null;\n var feed = __TA_PUBLIC_FEED_TABLES[__taNormalizeTable(tableName)];\n return feed || null;\n}\n\n// A feed is filtered and sorted only by the columns it serves: a condition on\n// any other column (or an expression over one) would answer for it.\nfunction __taAssertFeedFields(tableName, feed, fields) {\n for (var i = 0; i < fields.length; i++) {\n if (!fields[i]) continue;\n var column = String(fields[i]).trim().replace(/\"/g, '').toLowerCase();\n if (feed.columns.indexOf(column) === -1) __taForbid(__taNormalizeTable(tableName) + '.' + fields[i]);\n }\n}\n\n// null when the read may proceed, else { status, message }.\nasync function __taGuardRead(req, tableName, rawQuery) {\n if (typeof rawQuery === 'string' && rawQuery.length > 0 && __taBrowserIsReadOnly(req)) {\n return { status: 403, message: 'Forbidden: raw queries are only available to server-side calls' };\n }\n var touched = __taProtectedTablesRead(tableName, rawQuery);\n if (touched.length === 0) return null;\n if (__taIsInternalRequest(req)) return null;\n var token = null;\n try {\n token = await __tqSessionToken(req);\n } catch (e) {\n token = null;\n }\n if (!token) return { status: 401, message: 'Unauthenticated' };\n var role = __taRoleOf(token);\n if (!role || __TA_TRUSTED_READER_ROLES.indexOf(role) === -1) {\n return { status: 403, message: 'Forbidden: ' + touched[0] + ' is only readable by the store administrators' };\n }\n return null;\n}\n");
383
+ return "// GENERATED \u2014 see generateTableAccessHelperCode in\n// @teleporthq/teleport-shared/src/utils/table-access.ts.\n//\n// Tables that ARE money, and tables that decide how much of it is given away.\n// A browser reads the first group only as a trusted role (the generated admin)\n// and writes the second group never; server-side workflow segments present the\n// app secret instead. One list for both data-route families.\nvar __TA_PROTECTED_TABLES = ".concat(JSON.stringify(PROTECTED_TABLES), ";\nvar __TA_WRITE_PROTECTED_TABLES = ").concat(JSON.stringify(WRITE_PROTECTED_TABLES), ";\n// Readable from a browser only as this projection of the rows the database\n// calls live \u2014 see PUBLIC_FEED_TABLES. The per-table routes serve a whole\n// table (and raw SQL), which cannot express a feed, so there they are refused\n// like a protected table.\nvar __TA_PUBLIC_FEED_TABLES = ").concat(JSON.stringify(PUBLIC_FEED_TABLES), ";\nvar __TA_FEED_TABLES = Object.keys(__TA_PUBLIC_FEED_TABLES);\nvar __TA_TRUSTED_READER_ROLES = ").concat(JSON.stringify(params.trustedReaderRoles), ";\nvar __TA_CREDENTIAL_COLUMNS = ").concat(JSON.stringify(AUTH_CREDENTIAL_COLUMNS), ";\nvar __TA_UNAMBIGUOUS_CREDENTIAL_COLUMNS = ").concat(JSON.stringify(UNAMBIGUOUS_CREDENTIAL_COLUMNS), ";\nvar __TA_CREDENTIAL_TABLES = ").concat(JSON.stringify(AUTH_CREDENTIAL_TABLES), ";\nvar __TA_SQL_WRITE_RE = /\\b(?:insert|update|delete|merge|truncate)\\b/i;\n// What a statement can reach without spelling a table's name where this scan\n// can see it: a Unicode-escaped identifier (U&\"\\0075sers\" is `users`), the\n// functions that run SQL text or dump whole tables, schemas or the database\n// (the table name then sits in a string, or is not named at all), and the\n// planner statistics, which hold sampled column values of every table.\nvar __TA_OPAQUE_SQL_RE = /u&\"|\\b(?:query|table|cursor|schema|database)_to_xml\\w*\\s*\\(|\\bts_(?:stat|rewrite)\\s*\\(|\\bdblink\\w*\\s*\\(|\\bpg_read_(?:binary_)?file\\s*\\(|\\blo_(?:import|export|get)\\s*\\(|\\bpg_stat(?:s|istic)\\w*/i;\n\n// Only server code can read NEXTAUTH_SECRET, so a browser cannot forge the\n// header a server segment sends. An in-process call (getStaticProps and the\n// page-load workflows call the handler directly with a synthetic request)\n// marks its request object instead \u2014 a property, never a header, so nothing\n// on the wire can claim it.\nfunction __taPresentsAppSecret(req) {\n var secret = req && req.headers && req.headers['x-internal-data-secret'];\n return !!(secret && process.env.NEXTAUTH_SECRET && secret === process.env.NEXTAUTH_SECRET);\n}\n\nfunction __taIsRestrictedRequest(req) {\n return __taPresentsAppSecret(req) && !!(req.headers && req.headers['x-tq-restricted-sql'] === '1');\n}\n\nfunction __taIsInternalRequest(req) {\n if (req && req.__tqServerCall === true) return true;\n return __taPresentsAppSecret(req) && !__taIsRestrictedRequest(req);\n}\n\n// A page's own data, read to render it (the module's `fetchData(params, { as:\n// 'visitor' })`): served what an anonymous visitor is, through a statement the\n// page itself wrote. Marked on the request object, like an internal call, so\n// nothing on the wire can claim it.\nfunction __taIsVisitorView(req) {\n return !!(req && req.__tqVisitorView === true);\n}\n\n// Generated browser code reads through the structured routes and never sends\n// SQL or a write of its own \u2014 every data node runs server-side and presents\n// the secret. So where a secret exists to tell the two apart, anything else a\n// browser sends is refused whole: no statement scan has to be right about it.\n// An app without one (no authentication, hence no store) keeps the scan.\nfunction __taBrowserIsReadOnly(req) {\n return !!process.env.NEXTAUTH_SECRET && !__taIsInternalRequest(req) && !__taIsRestrictedRequest(req);\n}\n\n// Comments and string literals out, so a table name inside a string neither\n// hides a reference nor fakes one. Quoted identifiers stay, whole: in Postgres\n// they name tables, which is exactly what is being looked for.\n//\n// \u26D4 This is a lexer, not a search: a quote or comment marker inside a quoted\n// identifier (\"x'\", \"--\") is part of the name, a `$` or an `E` inside an\n// identifier (a$b$, ELSE'\u2026') starts neither a dollar quote nor an escape\n// string, and a dollar-quoted body is consumed by its own tag. An UNTERMINATED\n// literal, identifier, comment or dollar quote makes the whole scan fail closed\n// (ok:false): a statement this cleaner cannot read is one it cannot vouch for.\nfunction __taCleanSql(sql) {\n var text = String(sql || '');\n var out = '';\n var i = 0;\n function inIdentifier(at) { return at > 0 && /[A-Za-z0-9_$]/.test(text[at - 1]); }\n while (i < text.length) {\n var ch = text[i];\n var next = text[i + 1];\n if (ch === '-' && next === '-') {\n while (i < text.length && text[i] !== '\\n') i++;\n continue;\n }\n if (ch === '/' && next === '*') {\n i += 2;\n var depth = 1;\n while (i < text.length && depth > 0) {\n if (text[i] === '/' && text[i + 1] === '*') { depth++; i += 2; continue; }\n if (text[i] === '*' && text[i + 1] === '/') { depth--; i += 2; continue; }\n i++;\n }\n if (depth > 0) return { ok: false, text: out };\n out += ' ';\n continue;\n }\n // \"\u2026\" is one identifier token; \"\" inside it is an escaped quote.\n if (ch === '\"') {\n var end = i + 1;\n var closedIdentifier = false;\n while (end < text.length) {\n if (text[end] === '\"' && text[end + 1] === '\"') { end += 2; continue; }\n if (text[end] === '\"') { closedIdentifier = true; break; }\n end++;\n }\n if (!closedIdentifier) return { ok: false, text: out };\n out += text.slice(i, end + 1);\n i = end + 1;\n continue;\n }\n // $tag$ \u2026 $tag$ \u2014 the body is data whatever it holds.\n if (ch === '$' && !inIdentifier(i)) {\n var tagMatch = /^\\$[A-Za-z_][A-Za-z0-9_]*\\$|^\\$\\$/.exec(text.slice(i));\n if (tagMatch) {\n var tag = tagMatch[0];\n var close = text.indexOf(tag, i + tag.length);\n if (close === -1) return { ok: false, text: out };\n i = close + tag.length;\n out += ' ';\n continue;\n }\n }\n // E'\u2026' understands backslash escapes; a plain '\u2026' doubles the quote.\n if (ch === \"'\" || ((ch === 'E' || ch === 'e') && next === \"'\" && !inIdentifier(i))) {\n var escapes = ch !== \"'\";\n if (escapes) i++;\n i++;\n var closed = false;\n while (i < text.length) {\n if (escapes && text[i] === '\\\\') { i += 2; continue; }\n if (text[i] === \"'\" && text[i + 1] === \"'\") { i += 2; continue; }\n if (text[i] === \"'\") { i++; closed = true; break; }\n i++;\n }\n if (!closed) return { ok: false, text: out };\n out += ' ';\n continue;\n }\n out += ch;\n i++;\n }\n return { ok: true, text: out };\n}\n\nfunction __taNormalizeTable(tableName) {\n var name = String(tableName || '').trim().toLowerCase();\n var parts = name.split('.');\n return parts[parts.length - 1].replace(/\"/g, '');\n}\n\n// An unreadable statement, or one that can reach a table without naming it,\n// counts as mentioning EVERY protected table: fail closed rather than let a\n// spelling this scan cannot see decide it is harmless.\nfunction __taSqlMentionsTable(sql, table) {\n var cleaned = __taCleanSql(sql);\n if (!cleaned.ok || __TA_OPAQUE_SQL_RE.test(cleaned.text)) return true;\n var pattern = new RegExp('(?:^|[^A-Za-z0-9_])\"?' + table + '\"?(?![A-Za-z0-9_])', 'i');\n return pattern.test(cleaned.text);\n}\n\nfunction __taIsCredentialTable(tableName) {\n return __TA_CREDENTIAL_TABLES.indexOf(__taNormalizeTable(tableName)) !== -1;\n}\n\n// Credentials never travel in a row: a column is dropped by name, however the\n// caller spelled it in its projection.\nfunction __taWithoutCredentials(rows, tableName) {\n if (!Array.isArray(rows)) return rows;\n var dropped = tableName && __taIsCredentialTable(tableName)\n ? __TA_CREDENTIAL_COLUMNS\n : tableName ? [] : __TA_UNAMBIGUOUS_CREDENTIAL_COLUMNS;\n if (dropped.length === 0) return rows;\n return rows.map(function(row) {\n if (!row || typeof row !== 'object') return row;\n var copy = {};\n Object.keys(row).forEach(function(key) {\n if (dropped.indexOf(String(key).toLowerCase()) === -1) copy[key] = row[key];\n });\n return copy;\n });\n}\n\nfunction __taForbid(table) {\n var err = new Error('Forbidden: ' + table + ' is only accessible to server-side workflow nodes');\n err.status = 403;\n throw err;\n}\n\nfunction __taAssertSqlTextAllowed(sql) {\n var text = String(sql || '');\n // A statement can hand back a whole row (`SELECT u FROM users u`,\n // `row_to_json(u)`) without naming a credential column, so the table itself\n // is out of reach of anything but the server's own calls.\n for (var ci = 0; ci < __TA_CREDENTIAL_TABLES.length; ci++) {\n if (__taSqlMentionsTable(text, __TA_CREDENTIAL_TABLES[ci])) __taForbid(__TA_CREDENTIAL_TABLES[ci]);\n }\n for (var pi = 0; pi < __TA_PROTECTED_TABLES.length; pi++) {\n if (__taSqlMentionsTable(text, __TA_PROTECTED_TABLES[pi])) __taForbid(__TA_PROTECTED_TABLES[pi]);\n }\n // A feed table has exactly one browser-readable shape, and a statement the\n // caller wrote is not it.\n for (var fi = 0; fi < __TA_FEED_TABLES.length; fi++) {\n if (__taSqlMentionsTable(text, __TA_FEED_TABLES[fi])) __taForbid(__TA_FEED_TABLES[fi]);\n }\n var cleaned = __taCleanSql(text);\n // Unreadable, or a write: either way the write-protected tables are refused.\n if (cleaned.ok && !__TA_SQL_WRITE_RE.test(cleaned.text)) return;\n for (var wi = 0; wi < __TA_WRITE_PROTECTED_TABLES.length; wi++) {\n if (__taSqlMentionsTable(text, __TA_WRITE_PROTECTED_TABLES[wi])) __taForbid(__TA_WRITE_PROTECTED_TABLES[wi]);\n }\n}\n\nfunction __taRoleOf(token) {\n if (!token || typeof token !== 'object') return null;\n if (typeof token.role === 'string') return token.role;\n if (typeof token.roleName === 'string') return token.roleName;\n if (Array.isArray(token.roles) && typeof token.roles[0] === 'string') return token.roles[0];\n return null;\n}\n\n// The tables a read of `tableName` (or the raw statement) touches that this\n// route family may not serve to a browser: the protected ones, and the feed\n// tables, whose one browser-readable projection lives on the data API instead.\nvar __TA_ROUTE_PROTECTED_TABLES = __TA_PROTECTED_TABLES.concat(__TA_FEED_TABLES);\n\nfunction __taProtectedTablesRead(tableName, rawQuery) {\n var touched = [];\n var bare = __taNormalizeTable(tableName);\n if (__TA_ROUTE_PROTECTED_TABLES.indexOf(bare) !== -1) touched.push(bare);\n if (typeof rawQuery === 'string' && rawQuery.length > 0) {\n for (var i = 0; i < __TA_ROUTE_PROTECTED_TABLES.length; i++) {\n if (touched.indexOf(__TA_ROUTE_PROTECTED_TABLES[i]) === -1 && __taSqlMentionsTable(rawQuery, __TA_ROUTE_PROTECTED_TABLES[i])) {\n touched.push(__TA_ROUTE_PROTECTED_TABLES[i]);\n }\n }\n }\n return touched;\n}\n\n// The projection an untrusted caller gets of a feed table, or null when the\n// table has no feed or the caller is a server-side one reading it whole.\nfunction __taPublicFeed(req, tableName) {\n if (__taIsInternalRequest(req)) return null;\n var feed = __TA_PUBLIC_FEED_TABLES[__taNormalizeTable(tableName)];\n return feed || null;\n}\n\n// A feed is filtered and sorted only by the columns it serves: a condition on\n// any other column (or an expression over one) would answer for it.\nfunction __taAssertFeedFields(tableName, feed, fields) {\n for (var i = 0; i < fields.length; i++) {\n if (!fields[i]) continue;\n var column = String(fields[i]).trim().replace(/\"/g, '').toLowerCase();\n if (feed.columns.indexOf(column) === -1) __taForbid(__taNormalizeTable(tableName) + '.' + fields[i]);\n }\n}\n\n// null when the read may proceed, else { status, message }.\nasync function __taGuardRead(req, tableName, rawQuery) {\n var visitorView = __taIsVisitorView(req);\n if (typeof rawQuery === 'string' && rawQuery.length > 0 && __taBrowserIsReadOnly(req) && !visitorView) {\n return { status: 403, message: 'Forbidden: raw queries are only available to server-side calls' };\n }\n var touched = __taProtectedTablesRead(tableName, rawQuery);\n if (touched.length === 0) return null;\n if (__taIsInternalRequest(req)) return null;\n if (visitorView) {\n // A page reads its members' public profiles: the auth users table alone,\n // through the structured read, which the route's row policy projects onto\n // the public profile columns. Nothing else protected, and no statement.\n var readsProfiles = !(typeof rawQuery === 'string' && rawQuery.length > 0) &&\n touched.length === 1 && __taIsCredentialTable(tableName);\n // A visitor is nobody in particular: no session to read.\n return readsProfiles ? null : { status: 401, message: 'Unauthenticated' };\n }\n var token = null;\n try {\n token = await __tqSessionToken(req);\n } catch (e) {\n token = null;\n }\n if (!token) return { status: 401, message: 'Unauthenticated' };\n var role = __taRoleOf(token);\n if (!role || __TA_TRUSTED_READER_ROLES.indexOf(role) === -1) {\n return { status: 403, message: 'Forbidden: ' + touched[0] + ' is only readable by the store administrators' };\n }\n return null;\n}\n");
270
384
  };
271
385
  //# sourceMappingURL=table-access.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"table-access.js","sourceRoot":"","sources":["../../../src/utils/table-access.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"table-access.js","sourceRoot":"","sources":["../../../src/utils/table-access.ts"],"names":[],"mappings":";;;;;;;;;AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAA;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,CAAC,IAAM,YAAY,GAA0B;IACjD,qBAAqB;IACrB,iCAAiC;IACjC,mBAAmB;IACnB,8BAA8B;IAC9B,uEAAuE;IACvE,sEAAsE;IACtE,0DAA0D;IAC1D,sBAAsB;IACtB,0EAA0E;IAC1E,qEAAqE;IACrE,uBAAuB;IACvB,wEAAwE;IACxE,qEAAqE;IACrE,yDAAyD;IACzD,wBAAwB;IACxB,uBAAuB;CACxB,CAAA;AAED,MAAM,CAAC,IAAM,sBAAsB;IACjC,OAAO;IACP,iBAAiB;IACjB,sBAAsB;IACtB,mBAAmB;IACnB,wBAAwB;IACxB,iBAAiB;IACjB,oBAAoB;IACpB,yBAAyB;IACzB,8BAA8B;IAC9B,mCAAmC;IACnC,0BAA0B;IAC1B,eAAe;IACf,qBAAqB;IACrB,qBAAqB;IACrB,8EAA8E;IAC9E,gEAAgE;IAChE,wBAAwB;IACxB,oBAAoB;IACpB,uEAAuE;IACvE,qEAAqE;IACrE,6BAA6B;IAC7B,0BAA0B;IAC1B,0EAA0E;IAC1E,0EAA0E;IAC1E,6BAA6B;GAK1B,uBAAuB;IAC1B,4EAA4E;IAC5E,mEAAmE;IACnE,mDAAmD;IACnD,oCAAoC;IACpC,4BAA4B;IAC5B,gCAAgC;IAChC,2BAA2B;IAC3B,wEAAwE;IACxE,4EAA4E;IAC5E,yEAAyE;IACzE,yEAAyE;IACzE,8BAA8B;IAC9B,mBAAmB;IACnB,wBAAwB;IACxB,2BAA2B;IAC3B,0EAA0E;IAC1E,4EAA4E;IAC5E,gEAAgE;IAChE,4BAA4B;IAC5B,kCAAkC;SACnC,CAAA;AAED,MAAM,CAAC,IAAM,gBAAgB,mCAA8B,YAAY,SAAK,sBAAsB,OAAC,CAAA;AAEnG,MAAM,CAAC,IAAM,sBAAsB,GAA0B,CAAC,oBAAoB,CAAC,CAAA;AAEnF;;;;;GAKG;AACH,MAAM,CAAC,IAAM,uBAAuB,GAA0B;IAC5D,UAAU;IACV,cAAc;IACd,eAAe;IACf,UAAU;IACV,eAAe;IACf,YAAY;IACZ,OAAO;IACP,YAAY;CACb,CAAA;AAED,0EAA0E;AAC1E,MAAM,CAAC,IAAM,sBAAsB,GAA0B,CAAC,OAAO,CAAC,CAAA;AAEtE;;;;;GAKG;AACH,IAAM,8BAA8B,GAAG,uBAAuB,CAAC,MAAM,CACnE,UAAC,MAAM,IAAK,OAAA,MAAM,KAAK,OAAO,IAAI,MAAM,KAAK,YAAY,EAA7C,CAA6C,CAC1D,CAAA;AASD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,IAAM,kBAAkB,GAA8C;IAC3E,kBAAkB,EAAE;QAClB,+EAA+E;QAC/E,uEAAuE;QACvE,wEAAwE;QACxE,YAAY;QACZ,OAAO,EAAE;YACP,IAAI;YACJ,MAAM;YACN,QAAQ;YACR,UAAU;YACV,eAAe;YACf,gBAAgB;YAChB,yBAAyB;YACzB,aAAa;YACb,cAAc;YACd,iBAAiB;YACjB,cAAc;YACd,kBAAkB;YAClB,cAAc;YACd,cAAc;YACd,WAAW;SACZ;QACD,SAAS,EACP,0GAA0G;KAC7G;CACF,CAAA;AAED,oFAAoF;AACpF,IAAM,iBAAiB,GAAG,aAAa,CAAA;AACvC,IAAM,kBAAkB,GAAG,QAAQ,CAAA;AAEnC;;;;;;;;;;GAUG;AACH,MAAM,CAAC,IAAM,sBAAsB,GAAG,UAAC,IAAgC;IACrE,IAAI,CAAC,IAAI,EAAE;QACT,OAAO,EAAE,CAAA;KACV;IACD,IAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAA;IACrC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,gBAAgB,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,UAAC,MAAM;QACxD,IAAI,MAAM,IAAI,MAAM,CAAC,UAAU,KAAK,iBAAiB,EAAE;YACrD,CAAC;YAAA,CAAC,MAAM,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,UAAC,IAAI,IAAK,OAAA,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,EAArB,CAAqB,CAAC,CAAA;SACtE;IACH,CAAC,CAAC,CAAA;IACF,IAAI,WAAW,CAAC,IAAI,GAAG,CAAC,EAAE;QACxB,OAAO,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,IAAI,EAAE,CAAA;KACtC;IAED,IAAI,eAAe,GAAoB,IAAI,CAAA;4BAChC,IAAI;QACb,IAAM,KAAK,GAAG,IAAI,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAA;QACtE,IAAI,KAAK,KAAK,kBAAkB,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,UAAG,kBAAkB,MAAG,CAAC,EAAE;;SAEhF;QACD,IAAM,SAAS,GAAG,IAAI,CAAC,YAAY,IAAI,EAAE,CAAA;QACzC,eAAe;YACb,eAAe,KAAK,IAAI;gBACtB,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC;gBAChC,CAAC,CAAC,eAAe,CAAC,MAAM,CAAC,UAAC,IAAI,IAAK,OAAA,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAA9B,CAA8B,CAAC,CAAA;;IATxE,KAAmB,UAAwC,EAAxC,KAAA,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,cAAc,IAAI,EAAE,CAAC,EAAxC,cAAwC,EAAxC,IAAwC;QAAtD,IAAM,IAAI,SAAA;gBAAJ,IAAI;KAUd;IACD,OAAO,CAAC,eAAe,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;AACvC,CAAC,CAAA;AAED;;;;;;GAMG;AACH,MAAM,CAAC,IAAM,yBAAyB,GAAG,UAAC,IAAgC;IACxE,OAAA,sBAAsB,CAAC,IAAI,CAAC;AAA5B,CAA4B,CAAA;AAE9B;;;;;;GAMG;AACH,MAAM,CAAC,IAAM,sBAAsB,GAA0B;IAC3D,IAAI;IACJ,MAAM;IACN,cAAc;IACd,OAAO;IACP,QAAQ;IACR,YAAY;IACZ,KAAK;CACN,CAAA;AAED,IAAM,aAAa,GAAG,UAAC,SAAiB;IACtC,IAAM,KAAK,GAAG,MAAM,CAAC,SAAS,IAAI,EAAE,CAAC;SAClC,IAAI,EAAE;SACN,WAAW,EAAE;SACb,KAAK,CAAC,GAAG,CAAC,CAAA;IACb,OAAO,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;AAClD,CAAC,CAAA;AAED;;;;;GAKG;AACH,MAAM,CAAC,IAAM,oBAAoB,GAAG,UAAC,SAAiB;IACpD,IAAM,IAAI,GAAG,aAAa,CAAC,SAAS,CAAC,CAAA;IACrC,IAAI,sBAAsB,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE;QAC/C,OAAO,KAAK,CAAA;KACb;IACD,OAAO,CACL,gBAAgB,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrC,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,kBAAkB,EAAE,IAAI,CAAC,CAC/D,CAAA;AACH,CAAC,CAAA;AAED,IAAM,YAAY,GAAG,UAAC,KAAa;IACjC,OAAA,KAAK,KAAK,kBAAkB,IAAI,KAAK,CAAC,UAAU,CAAC,UAAG,kBAAkB,MAAG,CAAC;AAA1E,CAA0E,CAAA;AAO5E;;;;;;GAMG;AACH,IAAM,oBAAoB,GAAG,UAAC,IAAwB;IACpD,IAAM,KAAK,GAAG,IAAI,GAAG,EAAqB,CAAA;IAC1C,IAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,gBAAgB,IAAI,EAAE,CAAC,CAAA;IAC1D,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,cAAc,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,UAAC,EAAc;YAAb,MAAM,QAAA,EAAE,IAAI,QAAA;QAC9D,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,IAAI,CAAC,cAAc,EAAE;YAClE,OAAM;SACP;QACD,IAAM,WAAW,GAAG,OAAO;aACxB,MAAM,CAAC,UAAC,MAAM,YAAK,OAAA,CAAA,MAAA,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,QAAQ,0CAAG,MAAM,CAAC,MAAK,MAAM,CAAA,EAAA,CAAC;aACzD,OAAO,CAAC,UAAC,MAAM,IAAK,OAAA,MAAM,CAAC,YAAY,IAAI,EAAE,EAAzB,CAAyB,CAAC,CAAA;QACjD,IAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACtC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE;YACpB,YAAY,EAAE,CAAC,CAAC,CAAA,QAAQ,aAAR,QAAQ,uBAAR,QAAQ,CAAE,YAAY,CAAA,IAAI,CAAC,CAAC,IAAI,CAAC,YAAY;YAC7D,KAAK,EAAE,KAAK,CAAC,IAAI,CACf,IAAI,GAAG,+CAAK,CAAC,CAAA,QAAQ,aAAR,QAAQ,uBAAR,QAAQ,CAAE,KAAK,KAAI,EAAE,CAAC,SAAK,CAAC,IAAI,CAAC,YAAY,IAAI,EAAE,CAAC,SAAK,WAAW,QAAE,CACpF;SACF,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IACF,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE;QACjF,IAAM,UAAU,GAAG,sBAAsB,CAAC,IAAI,CAAC,CAAA;QAC/C,KAAK,CAAC,GAAG,CAAC,kBAAkB,EAAE;YAC5B,YAAY,EAAE,IAAI;YAClB,KAAK,EAAE,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;SACtD,CAAC,CAAA;KACH;IACD,OAAO,KAAK,CAAA;AACd,CAAC,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,IAAM,gBAAgB,GAAG,UAC9B,IAAyC,EACzC,IAAgC;;IAEhC,IAAI,CAAC,IAAI,EAAE;QACT,OAAO,KAAK,CAAA;KACb;IACD,IAAM,YAAY,GAAG,yBAAyB,CAAC,IAAI,CAAC,CAAA;IACpD,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE;QAC7B,OAAO,KAAK,CAAA;KACb;IACD,IAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,MAAA,IAAI,CAAC,cAAc,0CAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACxE,IAAI,GAAG,aAAH,GAAG,uBAAH,GAAG,CAAE,cAAc,EAAE;QACvB,OAAO,KAAK,CAAA;KACb;IACD,IAAM,KAAK,GAAG,OAAO,CAAA,GAAG,aAAH,GAAG,uBAAH,GAAG,CAAE,KAAK,CAAA,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAA;IACrE,IAAI,CAAC,KAAK,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,EAAE;QAClC,OAAO,KAAK,CAAA;KACb;IACD,sFAAsF;IACtF,IAAM,KAAK,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAA;IACxC,IAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;SACtC,MAAM,CAAC,UAAC,KAAK,IAAK,OAAA,KAAK,CAAC,UAAU,CAAC,UAAG,KAAK,MAAG,CAAC,EAA7B,CAA6B,CAAC;SAChD,IAAI,CAAC,UAAC,CAAC,EAAE,CAAC,IAAK,OAAA,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,EAAnB,CAAmB,CAAC,CAAC,CAAC,CAAC,CAAA;IACzC,IAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;IAC7E,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE;QAC1D,OAAO,KAAK,CAAA;KACb;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,UAAC,IAAI,IAAK,OAAA,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAjC,CAAiC,CAAC,CAAA;AACtE,CAAC,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,CAAC,IAAM,6BAA6B,GAAG,UAAC,MAE7C;IACC,OAAO,gdAOqB,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC,kDAC1B,IAAI,CAAC,SAAS,CAAC,sBAAsB,CAAC,wTAK1C,IAAI,CAAC,SAAS,CAAC,kBAAkB,CAAC,8GAEhC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,kBAAkB,CAAC,8CAC3C,IAAI,CAAC,SAAS,CAAC,uBAAuB,CAAC,0DAC3B,IAAI,CAAC,SAAS,CAAC,8BAA8B,CAAC,6CAC3D,IAAI,CAAC,SAAS,CAAC,sBAAsB,CAAC,o7XA+QpE,CAAA;AACD,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teleporthq/teleport-shared",
3
- "version": "0.44.0",
3
+ "version": "0.44.1",
4
4
  "description": "A utility belt for the entire teleportHQ ecosystem",
5
5
  "author": "teleportHQ",
6
6
  "license": "MIT",
@@ -29,5 +29,5 @@
29
29
  "jss": "^10.0.0",
30
30
  "jss-preset-default": "^10.0.0"
31
31
  },
32
- "gitHead": "3f8ab44ace8163b154799b7ea68ece45b3cf9022"
32
+ "gitHead": "f17c2064dd4ed978d9a022e98777a7ebf14d3e63"
33
33
  }
package/src/index.ts CHANGED
@@ -25,6 +25,7 @@ import * as SessionCookieResolver from './utils/session-cookie-resolver'
25
25
  import * as SubscriptionAccess from './utils/subscription-access'
26
26
  import * as FontPreconnect from './utils/font-preconnect'
27
27
  import * as EmailDate from './utils/email-date'
28
+ import * as EmailSender from './utils/email-sender'
28
29
  import * as RichTextEmbeds from './utils/rich-text-embeds'
29
30
  import * as RichTextEmbedsCodegen from './utils/rich-text-embeds-codegen'
30
31
  import * as RichTextContent from './utils/rich-text-content'
@@ -67,6 +68,7 @@ export {
67
68
  SubscriptionAccess,
68
69
  FontPreconnect,
69
70
  EmailDate,
71
+ EmailSender,
70
72
  RichTextEmbeds,
71
73
  RichTextEmbedsCodegen,
72
74
  RichTextContent,
@@ -18,13 +18,16 @@ import * as PageTransition from './variants'
18
18
  * arriving page; the first animates over [0, D], the second waits D
19
19
  * and animates over [D, 2D]. Same order, same timing.
20
20
  *
21
- * The reveals (Circle, the Wipes) are the exception, in both exports: the
22
- * arriving page is revealed OVER the leaving one, which holds still underneath
23
- * for the whole run. Played one after the other, the leaving page was wiped
24
- * away to the site's bare background and the arriving one wiped in over it —
25
- * a white sheet between every two pages. Here the leaving picture keeps no
26
- * animation (the browser would otherwise fade it out under the arriving one)
27
- * and the arriving picture opens over [0, D].
21
+ * The reveals (Circle, the Wipes) and the dissolves (Fade, Crossfade) are the
22
+ * exception, in both exports: the arriving page comes in OVER the leaving one,
23
+ * which holds still underneath for the whole run. Played one after the other,
24
+ * the leaving page was wiped or faded away to the site's bare background before
25
+ * the arriving one came in — an empty screen between every two pages. Here the
26
+ * leaving picture keeps no animation (the browser would otherwise fade it out
27
+ * under the arriving one) and the arriving picture comes in over [0, D]. A
28
+ * picture of a page is opaque (the site's background included), so a dissolve
29
+ * shows the two pages mixed and never the background between them; faded both
30
+ * at once, the background showed through the two.
28
31
  *
29
32
  * A browser without the feature (Firefox today) simply navigates, which is
30
33
  * also what a visitor who asked for less motion gets.
@@ -81,6 +84,10 @@ const COVER_CLIPS: Record<string, { covered: string; opening: string }> = {
81
84
  }
82
85
  const OPEN = 'inset(0 0 0 0)'
83
86
 
87
+ /** A dissolve (Fade, Crossfade): the arriving page fades in over the leaving one, which holds. */
88
+ export const pageTransitionDissolves = (preset: string): boolean =>
89
+ preset === 'fade' || preset === 'crossfade'
90
+
84
91
  interface Animations {
85
92
  leave: [State, State]
86
93
  arrive: [State, State]
@@ -128,6 +135,8 @@ export interface ViewTransitionPlan {
128
135
  flies: boolean
129
136
  /** The arriving page is revealed over the leaving one, which holds (Circle, the Wipes). */
130
137
  revealsOver: boolean
138
+ /** The arriving page comes in over the leaving one, which holds: a reveal, or a dissolve (Fade, Crossfade). */
139
+ arrivesOver: boolean
131
140
  }
132
141
 
133
142
  export const viewTransitionPlan = (
@@ -170,20 +179,21 @@ export const viewTransitionPlan = (
170
179
  : null
171
180
 
172
181
  const timing = `${duration}s cubic-bezier(${curve.join(', ')})`
173
- // A reveal opens the arriving page over the leaving one: only the arrival
174
- // animates, from the start, and the leaving picture just stays.
182
+ // A reveal or a dissolve brings the arriving page in over the leaving one:
183
+ // only the arrival animates, from the start, and the leaving picture just stays.
175
184
  const revealsOver = !COVER_CLIPS[preset] && forward.arrive[1].clipPath !== undefined
176
- const arriveTiming = revealsOver ? `${timing} both` : `${timing} ${duration}s both`
185
+ const arrivesOver = revealsOver || pageTransitionDissolves(preset)
186
+ const arriveTiming = arrivesOver ? `${timing} both` : `${timing} ${duration}s both`
177
187
  const rules = [
178
188
  // Opting every page in is for page-to-page navigations only; a single-page
179
189
  // app starts its own transitions and must not turn full reloads into them.
180
190
  ...(target.crossDocument ? ['@view-transition {\n navigation: auto;\n}'] : []),
181
191
  '::view-transition-old(root),\n::view-transition-new(root) {\n mix-blend-mode: normal;\n}',
182
- revealsOver
192
+ arrivesOver
183
193
  ? '::view-transition-old(root) {\n animation: none;\n}'
184
194
  : `::view-transition-old(root) {\n animation: tq-page-leave ${timing} both;\n}`,
185
195
  `::view-transition-new(root) {\n animation: tq-page-arrive ${arriveTiming};\n}`,
186
- ...(revealsOver ? [] : [keyframes('tq-page-leave', forward.leave[0], forward.leave[1])]),
196
+ ...(arrivesOver ? [] : [keyframes('tq-page-leave', forward.leave[0], forward.leave[1])]),
187
197
  keyframes('tq-page-arrive', forward.arrive[0], forward.arrive[1]),
188
198
  ]
189
199
  if (COVER_CLIPS[preset]) {
@@ -194,14 +204,14 @@ export const viewTransitionPlan = (
194
204
  )
195
205
  }
196
206
  if (back) {
197
- if (!revealsOver) {
207
+ if (!arrivesOver) {
198
208
  rules.push(
199
209
  `html[${BACK_ATTR}="back"]::view-transition-old(root) {\n animation-name: tq-page-leave-back;\n}`
200
210
  )
201
211
  }
202
212
  rules.push(
203
213
  `html[${BACK_ATTR}="back"]::view-transition-new(root) {\n animation-name: tq-page-arrive-back;\n}`,
204
- ...(revealsOver ? [] : [keyframes('tq-page-leave-back', back.leave[0], back.leave[1])]),
214
+ ...(arrivesOver ? [] : [keyframes('tq-page-leave-back', back.leave[0], back.leave[1])]),
205
215
  keyframes('tq-page-arrive-back', back.arrive[0], back.arrive[1])
206
216
  )
207
217
  }
@@ -212,13 +222,13 @@ export const viewTransitionPlan = (
212
222
  // with its page instead of lingering over the next one.
213
223
  const flies = config.flyingPictures !== false
214
224
  if (flies) {
215
- const flight = revealsOver ? duration : duration * 2
225
+ const flight = arrivesOver ? duration : duration * 2
216
226
  rules.push(
217
227
  `::view-transition-group(${MORPH_NAME}) {\n animation-duration: ${flight}s;\n animation-timing-function: cubic-bezier(${curve.join(
218
228
  ', '
219
229
  )});\n}`,
220
230
  `::view-transition-old(${MORPH_NAME}),\n::view-transition-new(${MORPH_NAME}) {\n height: 100%;\n object-fit: cover;\n overflow: clip;\n animation-duration: ${flight}s;\n}`,
221
- revealsOver
231
+ arrivesOver
222
232
  ? `::view-transition-old(${MORPH_NAME}):only-child {\n animation: tq-morph-away ${timing} both;\n}`
223
233
  : `::view-transition-old(${MORPH_NAME}):only-child {\n animation: tq-page-leave ${timing} both, tq-morph-away ${timing} both;\n}`,
224
234
  `::view-transition-new(${MORPH_NAME}):only-child {\n animation: tq-page-arrive ${arriveTiming}, tq-morph-in ${arriveTiming};\n}`,
@@ -233,5 +243,6 @@ export const viewTransitionPlan = (
233
243
  needsOrigin,
234
244
  flies,
235
245
  revealsOver,
246
+ arrivesOver,
236
247
  }
237
248
  }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The sender ("From") of an email a published site sends, as ES5 source for the
3
+ * generated runtime code that cannot import from this package: the workflow
4
+ * email nodes, the welcome and farewell routes, the invoice sender and the
5
+ * store's order-notification sender. One rule, so every path names the sender
6
+ * the same way.
7
+ *
8
+ * The editor stores the sender as an address plus a separate display name
9
+ * (`from` and `fromName`). A provider takes it either as one RFC 5322 mailbox
10
+ * string (Postmark, Resend, Mailgun, nodemailer) or as its two parts
11
+ * (SendGrid's `{ email, name }`, MailerSend's `Sender`), so the emitted
12
+ * `__emailSender({ from, name })` answers both: `{ email, name, header }`.
13
+ *
14
+ * - No name: the bare address, as before.
15
+ * - A name holding an RFC 5322 special — `( ) < > [ ] : ; @ \ , . "` — is
16
+ * quoted, its `"` and `\` escaped. Unquoted, `Smith, Jones & Co <a@x.test>`
17
+ * reaches the provider as two addresses.
18
+ * - Line breaks and other control characters become spaces: a header ends at a
19
+ * line break, so a name carrying one would break the header.
20
+ * - A `from` that already names its sender (`Shop <a@x.test>`) keeps that name
21
+ * and is never wrapped a second time; a name typed with its own quotes is not
22
+ * quoted twice.
23
+ *
24
+ * ⛔ ONE parameter. The source is appended to serialized node handlers whose
25
+ * entry point a minified build finds by arity (a 2-param declaration wins, see
26
+ * `resolveHandlerEntryName`), so a 2-param helper would be called in place of
27
+ * the handler. A string literal rather than a `.toString()`, so a consumer's
28
+ * minifier can neither drop nor rename the declaration (see
29
+ * `commerce-tracking.ts` in teleport-plugin-next-workflows).
30
+ */
31
+
32
+ /** What a caller passes: the stored address and display name. */
33
+ export interface EmailSenderInput {
34
+ from?: unknown
35
+ name?: unknown
36
+ }
37
+
38
+ /** What the emitted helper answers. */
39
+ export interface EmailSenderParts {
40
+ email: string
41
+ name: string
42
+ header: string
43
+ }
44
+
45
+ export const generateEmailSenderHelperCode = (): string => `
46
+ // The sender of an email as { email, name, header }: header is the mailbox for a
47
+ // provider taking the From as one string, email and name are its two parts.
48
+ function __emailSender(sender) {
49
+ function text(value) {
50
+ return typeof value === 'string' || typeof value === 'number' ? String(value) : '';
51
+ }
52
+ function plainName(value) {
53
+ var name = text(value).replace(/[\\s\\u0000-\\u001f\\u007f]+/g, ' ').trim();
54
+ var quoted = /^"((?:[^"\\\\]|\\\\.)*)"$/.exec(name);
55
+ return quoted ? quoted[1].replace(/\\\\(.)/g, '$1').trim() : name;
56
+ }
57
+ var email = text(sender && sender.from).trim();
58
+ var name = plainName(sender && sender.name);
59
+ var named = /^([^<>]*)<([^<>]*)>$/.exec(email);
60
+ if (named) {
61
+ email = named[2].trim();
62
+ name = plainName(named[1]) || name;
63
+ } else if (/[<>]/.test(email)) {
64
+ return { email: email, name: '', header: email };
65
+ }
66
+ if (!email || !name) {
67
+ return { email: email, name: '', header: email };
68
+ }
69
+ var display = /[()<>\\[\\]:;@\\\\,."]/.test(name)
70
+ ? '"' + name.replace(/(["\\\\])/g, '\\\\$1') + '"'
71
+ : name;
72
+ return { email: email, name: name, header: display + ' <' + email + '>' };
73
+ }
74
+ `
@@ -245,6 +245,135 @@ export const resolveAdminPanelRoles = (auth?: UIDLAuthentication | null): string
245
245
  export const resolveTrustedReaderRoles = (auth?: UIDLAuthentication | null): string[] =>
246
246
  resolveAdminPanelRoles(auth)
247
247
 
248
+ /**
249
+ * What a page shows of a member to whoever opens it: their public profile —
250
+ * who they are, never how to reach them. A page's own data, read for a visitor,
251
+ * carries only these columns of the auth users table: never an email, a phone
252
+ * or an address, a credential, a role or anything else of the account. The
253
+ * member's own private fields reach their own page from their session.
254
+ */
255
+ export const PUBLIC_PROFILE_COLUMNS: ReadonlyArray<string> = [
256
+ 'id',
257
+ 'name',
258
+ 'display_name',
259
+ 'image',
260
+ 'avatar',
261
+ 'avatar_url',
262
+ 'bio',
263
+ ]
264
+
265
+ const bareTableName = (tableName: string): string => {
266
+ const parts = String(tableName || '')
267
+ .trim()
268
+ .toLowerCase()
269
+ .split('.')
270
+ return parts[parts.length - 1].replace(/"/g, '')
271
+ }
272
+
273
+ /**
274
+ * The tables a page's own data, read for a visitor (`pageReadsAsStore` false),
275
+ * gets no row of — so it need not ask: the protected ones, and the feed tables,
276
+ * whose one visitor projection lives on the data API. Not the auth users table:
277
+ * a page reads its members' public profiles (`PUBLIC_PROFILE_COLUMNS`).
278
+ */
279
+ export const pageDataReadsNoRowOf = (tableName: string): boolean => {
280
+ const bare = bareTableName(tableName)
281
+ if (AUTH_CREDENTIAL_TABLES.indexOf(bare) !== -1) {
282
+ return false
283
+ }
284
+ return (
285
+ PROTECTED_TABLES.indexOf(bare) !== -1 ||
286
+ Object.prototype.hasOwnProperty.call(PUBLIC_FEED_TABLES, bare)
287
+ )
288
+ }
289
+
290
+ const isAdminRoute = (route: string): boolean =>
291
+ route === ADMIN_ROUTE_PREFIX || route.startsWith(`${ADMIN_ROUTE_PREFIX}/`)
292
+
293
+ interface RouteGate {
294
+ requiresAuth: boolean
295
+ roles: string[]
296
+ }
297
+
298
+ /**
299
+ * The gate the generated middleware puts on each route it knows
300
+ * (`generateMiddlewareFile`): a page's own protection, the roles of the
301
+ * role-gated folders it sits in merged in, and `/admin` — when any admin page
302
+ * is protected — admitting the admin panel's roles. A row-owned page is left
303
+ * to its own SQL there, so it gates nothing here either.
304
+ */
305
+ const middlewareRouteGates = (auth: UIDLAuthentication): Map<string, RouteGate> => {
306
+ const gates = new Map<string, RouteGate>()
307
+ const folders = Object.values(auth.folderProtection || {})
308
+ Object.entries(auth.pageProtection || {}).forEach(([pageId, page]) => {
309
+ if (!page || typeof page.route !== 'string' || page.rowOwnerColumn) {
310
+ return
311
+ }
312
+ const folderRoles = folders
313
+ .filter((folder) => folder?.children?.[pageId] === 'page')
314
+ .flatMap((folder) => folder.allowedRoles || [])
315
+ const existing = gates.get(page.route)
316
+ gates.set(page.route, {
317
+ requiresAuth: !!existing?.requiresAuth || !!page.requiresAuth,
318
+ roles: Array.from(
319
+ new Set([...(existing?.roles || []), ...(page.allowedRoles || []), ...folderRoles])
320
+ ),
321
+ })
322
+ })
323
+ if (!gates.has(ADMIN_ROUTE_PREFIX) && Array.from(gates.keys()).some(isAdminRoute)) {
324
+ const panelRoles = resolveAdminPanelRoles(auth)
325
+ gates.set(ADMIN_ROUTE_PREFIX, {
326
+ requiresAuth: true,
327
+ roles: panelRoles.length > 0 ? panelRoles : ['admin'],
328
+ })
329
+ }
330
+ return gates
331
+ }
332
+
333
+ /**
334
+ * Whether a page's own data — what its getStaticProps / getServerSideProps
335
+ * read to render it — may be read as the store rather than as a visitor.
336
+ *
337
+ * Only an admin panel page that nobody but the store's trusted readers may
338
+ * open: at `/admin` or below, behind the middleware, with every role its gate
339
+ * admits in `resolveTrustedReaderRoles`. Whoever receives that page's data
340
+ * could read the same rows through the data routes. Every other page — public,
341
+ * members-only, or an admin screen another role may open — is rendered with
342
+ * what a visitor is served, since its data reaches whoever opens it, and
343
+ * through `/_next/data` without rendering it at all. `route` is the page's
344
+ * path when its own protection does not name it.
345
+ */
346
+ export const pageReadsAsStore = (
347
+ page: { route?: string; pageId?: string },
348
+ auth?: UIDLAuthentication | null
349
+ ): boolean => {
350
+ if (!auth) {
351
+ return false
352
+ }
353
+ const trustedRoles = resolveTrustedReaderRoles(auth)
354
+ if (trustedRoles.length === 0) {
355
+ return false
356
+ }
357
+ const own = page.pageId ? auth.pageProtection?.[page.pageId] : undefined
358
+ if (own?.rowOwnerColumn) {
359
+ return false
360
+ }
361
+ const route = typeof own?.route === 'string' ? own.route : page.route
362
+ if (!route || !isAdminRoute(route)) {
363
+ return false
364
+ }
365
+ // The middleware's resolution: the route's own gate, else its nearest gated ancestor.
366
+ const gates = middlewareRouteGates(auth)
367
+ const ancestor = Array.from(gates.keys())
368
+ .filter((gated) => route.startsWith(`${gated}/`))
369
+ .sort((a, b) => b.length - a.length)[0]
370
+ const gate = gates.get(route) || (ancestor ? gates.get(ancestor) : undefined)
371
+ if (!gate || !gate.requiresAuth || gate.roles.length === 0) {
372
+ return false
373
+ }
374
+ return gate.roles.every((role) => trustedRoles.indexOf(role) !== -1)
375
+ }
376
+
248
377
  /**
249
378
  * ES5 source of the guard, inlined into a generated route. Declares:
250
379
  * - `__taIsInternalRequest(req)` — the caller presented the app secret, or
@@ -327,6 +456,14 @@ function __taIsInternalRequest(req) {
327
456
  return __taPresentsAppSecret(req) && !__taIsRestrictedRequest(req);
328
457
  }
329
458
 
459
+ // A page's own data, read to render it (the module's \`fetchData(params, { as:
460
+ // 'visitor' })\`): served what an anonymous visitor is, through a statement the
461
+ // page itself wrote. Marked on the request object, like an internal call, so
462
+ // nothing on the wire can claim it.
463
+ function __taIsVisitorView(req) {
464
+ return !!(req && req.__tqVisitorView === true);
465
+ }
466
+
330
467
  // Generated browser code reads through the structured routes and never sends
331
468
  // SQL or a write of its own — every data node runs server-side and presents
332
469
  // the secret. So where a secret exists to tell the two apart, anything else a
@@ -533,12 +670,22 @@ function __taAssertFeedFields(tableName, feed, fields) {
533
670
 
534
671
  // null when the read may proceed, else { status, message }.
535
672
  async function __taGuardRead(req, tableName, rawQuery) {
536
- if (typeof rawQuery === 'string' && rawQuery.length > 0 && __taBrowserIsReadOnly(req)) {
673
+ var visitorView = __taIsVisitorView(req);
674
+ if (typeof rawQuery === 'string' && rawQuery.length > 0 && __taBrowserIsReadOnly(req) && !visitorView) {
537
675
  return { status: 403, message: 'Forbidden: raw queries are only available to server-side calls' };
538
676
  }
539
677
  var touched = __taProtectedTablesRead(tableName, rawQuery);
540
678
  if (touched.length === 0) return null;
541
679
  if (__taIsInternalRequest(req)) return null;
680
+ if (visitorView) {
681
+ // A page reads its members' public profiles: the auth users table alone,
682
+ // through the structured read, which the route's row policy projects onto
683
+ // the public profile columns. Nothing else protected, and no statement.
684
+ var readsProfiles = !(typeof rawQuery === 'string' && rawQuery.length > 0) &&
685
+ touched.length === 1 && __taIsCredentialTable(tableName);
686
+ // A visitor is nobody in particular: no session to read.
687
+ return readsProfiles ? null : { status: 401, message: 'Unauthenticated' };
688
+ }
542
689
  var token = null;
543
690
  try {
544
691
  token = await __tqSessionToken(req);