@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.
- package/__tests__/utils/email-sender.ts +123 -0
- package/__tests__/utils/table-access-page-reads.ts +183 -0
- package/dist/cjs/index.d.ts +2 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/page-transition/view-transition-css.d.ts +4 -0
- package/dist/cjs/page-transition/view-transition-css.d.ts.map +1 -1
- package/dist/cjs/page-transition/view-transition-css.js +17 -10
- package/dist/cjs/page-transition/view-transition-css.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/utils/email-sender.d.ts +43 -0
- package/dist/cjs/utils/email-sender.d.ts.map +1 -0
- package/dist/cjs/utils/email-sender.js +36 -0
- package/dist/cjs/utils/email-sender.js.map +1 -0
- package/dist/cjs/utils/table-access.d.ts +32 -0
- package/dist/cjs/utils/table-access.d.ts.map +1 -1
- package/dist/cjs/utils/table-access.js +118 -2
- package/dist/cjs/utils/table-access.js.map +1 -1
- package/dist/esm/index.d.ts +2 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/page-transition/view-transition-css.d.ts +4 -0
- package/dist/esm/page-transition/view-transition-css.d.ts.map +1 -1
- package/dist/esm/page-transition/view-transition-css.js +15 -9
- package/dist/esm/page-transition/view-transition-css.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/utils/email-sender.d.ts +43 -0
- package/dist/esm/utils/email-sender.d.ts.map +1 -0
- package/dist/esm/utils/email-sender.js +32 -0
- package/dist/esm/utils/email-sender.js.map +1 -0
- package/dist/esm/utils/table-access.d.ts +32 -0
- package/dist/esm/utils/table-access.d.ts.map +1 -1
- package/dist/esm/utils/table-access.js +115 -1
- package/dist/esm/utils/table-access.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +2 -0
- package/src/page-transition/view-transition-css.ts +27 -16
- package/src/utils/email-sender.ts +74 -0
- 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":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teleporthq/teleport-shared",
|
|
3
|
-
"version": "0.44.
|
|
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": "
|
|
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)
|
|
22
|
-
* arriving page
|
|
23
|
-
* for the whole run. Played one after the other,
|
|
24
|
-
* away to the site's bare background
|
|
25
|
-
*
|
|
26
|
-
* animation (the browser would otherwise fade it out
|
|
27
|
-
* and the arriving picture
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
...(
|
|
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 (!
|
|
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
|
-
...(
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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);
|