@teleporthq/teleport-shared 0.43.67 → 0.43.69

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 (107) hide show
  1. package/__tests__/_helpers/embed-runtime-emitter.ts +197 -0
  2. package/__tests__/utils/discount-engine.ts +1548 -0
  3. package/__tests__/utils/product-options.ts +2458 -0
  4. package/__tests__/utils/rich-text-embeds-runtime-module.ts +34 -0
  5. package/__tests__/utils/subscription-access.ts +37 -0
  6. package/dist/cjs/index.d.ts +9 -1
  7. package/dist/cjs/index.d.ts.map +1 -1
  8. package/dist/cjs/index.js +17 -1
  9. package/dist/cjs/index.js.map +1 -1
  10. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  11. package/dist/cjs/utils/cart-currency.d.ts +27 -0
  12. package/dist/cjs/utils/cart-currency.d.ts.map +1 -0
  13. package/dist/cjs/utils/cart-currency.js +31 -0
  14. package/dist/cjs/utils/cart-currency.js.map +1 -0
  15. package/dist/cjs/utils/cart-line-pricing.d.ts +25 -0
  16. package/dist/cjs/utils/cart-line-pricing.d.ts.map +1 -0
  17. package/dist/cjs/utils/cart-line-pricing.js +29 -0
  18. package/dist/cjs/utils/cart-line-pricing.js.map +1 -0
  19. package/dist/cjs/utils/discount-engine.d.ts +74 -0
  20. package/dist/cjs/utils/discount-engine.d.ts.map +1 -0
  21. package/dist/cjs/utils/discount-engine.js +435 -0
  22. package/dist/cjs/utils/discount-engine.js.map +1 -0
  23. package/dist/cjs/utils/product-options.d.ts +25 -0
  24. package/dist/cjs/utils/product-options.d.ts.map +1 -0
  25. package/dist/cjs/utils/product-options.js +1155 -0
  26. package/dist/cjs/utils/product-options.js.map +1 -0
  27. package/dist/cjs/utils/rich-text-embeds-codegen.d.ts +13 -0
  28. package/dist/cjs/utils/rich-text-embeds-codegen.d.ts.map +1 -1
  29. package/dist/cjs/utils/rich-text-embeds-codegen.js +15 -104
  30. package/dist/cjs/utils/rich-text-embeds-codegen.js.map +1 -1
  31. package/dist/cjs/utils/rich-text-embeds-runtime-source.d.ts +2 -0
  32. package/dist/cjs/utils/rich-text-embeds-runtime-source.d.ts.map +1 -0
  33. package/dist/cjs/utils/rich-text-embeds-runtime-source.js +409 -0
  34. package/dist/cjs/utils/rich-text-embeds-runtime-source.js.map +1 -0
  35. package/dist/cjs/utils/session-cookie-resolver.d.ts +85 -0
  36. package/dist/cjs/utils/session-cookie-resolver.d.ts.map +1 -0
  37. package/dist/cjs/utils/session-cookie-resolver.js +108 -0
  38. package/dist/cjs/utils/session-cookie-resolver.js.map +1 -0
  39. package/dist/cjs/utils/subscription-access.d.ts +36 -0
  40. package/dist/cjs/utils/subscription-access.d.ts.map +1 -0
  41. package/dist/cjs/utils/subscription-access.js +61 -0
  42. package/dist/cjs/utils/subscription-access.js.map +1 -0
  43. package/dist/cjs/utils/table-access.d.ts +114 -0
  44. package/dist/cjs/utils/table-access.d.ts.map +1 -0
  45. package/dist/cjs/utils/table-access.js +238 -0
  46. package/dist/cjs/utils/table-access.js.map +1 -0
  47. package/dist/cjs/utils/web-push.d.ts +43 -0
  48. package/dist/cjs/utils/web-push.d.ts.map +1 -0
  49. package/dist/cjs/utils/web-push.js +62 -0
  50. package/dist/cjs/utils/web-push.js.map +1 -0
  51. package/dist/esm/index.d.ts +9 -1
  52. package/dist/esm/index.d.ts.map +1 -1
  53. package/dist/esm/index.js +9 -1
  54. package/dist/esm/index.js.map +1 -1
  55. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  56. package/dist/esm/utils/cart-currency.d.ts +27 -0
  57. package/dist/esm/utils/cart-currency.d.ts.map +1 -0
  58. package/dist/esm/utils/cart-currency.js +27 -0
  59. package/dist/esm/utils/cart-currency.js.map +1 -0
  60. package/dist/esm/utils/cart-line-pricing.d.ts +25 -0
  61. package/dist/esm/utils/cart-line-pricing.d.ts.map +1 -0
  62. package/dist/esm/utils/cart-line-pricing.js +25 -0
  63. package/dist/esm/utils/cart-line-pricing.js.map +1 -0
  64. package/dist/esm/utils/discount-engine.d.ts +74 -0
  65. package/dist/esm/utils/discount-engine.d.ts.map +1 -0
  66. package/dist/esm/utils/discount-engine.js +431 -0
  67. package/dist/esm/utils/discount-engine.js.map +1 -0
  68. package/dist/esm/utils/product-options.d.ts +25 -0
  69. package/dist/esm/utils/product-options.d.ts.map +1 -0
  70. package/dist/esm/utils/product-options.js +1151 -0
  71. package/dist/esm/utils/product-options.js.map +1 -0
  72. package/dist/esm/utils/rich-text-embeds-codegen.d.ts +13 -0
  73. package/dist/esm/utils/rich-text-embeds-codegen.d.ts.map +1 -1
  74. package/dist/esm/utils/rich-text-embeds-codegen.js +16 -105
  75. package/dist/esm/utils/rich-text-embeds-codegen.js.map +1 -1
  76. package/dist/esm/utils/rich-text-embeds-runtime-source.d.ts +2 -0
  77. package/dist/esm/utils/rich-text-embeds-runtime-source.d.ts.map +1 -0
  78. package/dist/esm/utils/rich-text-embeds-runtime-source.js +406 -0
  79. package/dist/esm/utils/rich-text-embeds-runtime-source.js.map +1 -0
  80. package/dist/esm/utils/session-cookie-resolver.d.ts +85 -0
  81. package/dist/esm/utils/session-cookie-resolver.d.ts.map +1 -0
  82. package/dist/esm/utils/session-cookie-resolver.js +103 -0
  83. package/dist/esm/utils/session-cookie-resolver.js.map +1 -0
  84. package/dist/esm/utils/subscription-access.d.ts +36 -0
  85. package/dist/esm/utils/subscription-access.d.ts.map +1 -0
  86. package/dist/esm/utils/subscription-access.js +57 -0
  87. package/dist/esm/utils/subscription-access.js.map +1 -0
  88. package/dist/esm/utils/table-access.d.ts +114 -0
  89. package/dist/esm/utils/table-access.d.ts.map +1 -0
  90. package/dist/esm/utils/table-access.js +233 -0
  91. package/dist/esm/utils/table-access.js.map +1 -0
  92. package/dist/esm/utils/web-push.d.ts +43 -0
  93. package/dist/esm/utils/web-push.d.ts.map +1 -0
  94. package/dist/esm/utils/web-push.js +59 -0
  95. package/dist/esm/utils/web-push.js.map +1 -0
  96. package/package.json +3 -3
  97. package/src/index.ts +16 -0
  98. package/src/utils/cart-currency.ts +76 -0
  99. package/src/utils/cart-line-pricing.ts +151 -0
  100. package/src/utils/discount-engine.ts +437 -0
  101. package/src/utils/product-options.ts +1150 -0
  102. package/src/utils/rich-text-embeds-codegen.ts +16 -183
  103. package/src/utils/rich-text-embeds-runtime-source.ts +405 -0
  104. package/src/utils/session-cookie-resolver.ts +305 -0
  105. package/src/utils/subscription-access.ts +59 -0
  106. package/src/utils/table-access.ts +516 -0
  107. package/src/utils/web-push.ts +61 -0
@@ -0,0 +1,516 @@
1
+ import type { UIDLAuthentication } from '@teleporthq/teleport-types'
2
+
3
+ /**
4
+ * Which store tables a browser may touch through the generated data routes.
5
+ *
6
+ * Every generated Next store exposes two families of data routes: the
7
+ * workflow data API (`/api/data/<source>/<op>`, called by workflow data nodes)
8
+ * and one read route per bound table (`/api/teleport-<table>-<source>`, called
9
+ * by the pages' data providers, with a `rawQuery` escape hatch). Both accept
10
+ * a browser's request. For most tables that is the product: a catalogue is
11
+ * public. For the tables below it is theft.
12
+ *
13
+ * `MONEY_TABLES` are money and the keys to it: a gift card's code and
14
+ * balance, its ledger, every voucher code (a generated batch is a list of
15
+ * single-use coupons) and the redemption ledger. A browser reads or writes
16
+ * them only as a signed-in member of a trusted role (the generated admin) —
17
+ * the server-side workflow segments that look a card up, debit it or redeem a
18
+ * code identify themselves with the app secret instead.
19
+ *
20
+ * `CUSTOMER_RECORD_TABLES` are the customers' own records: who ordered what
21
+ * and where it ships, their invoices, parcels, returns, carts and reviews,
22
+ * and the accounts themselves. A buyer is served their own order through the
23
+ * page-load workflow (a server segment), the merchant every order through
24
+ * the admin's session — nothing in a generated storefront lists them from
25
+ * the browser, so a browser that asks for the list is not the store.
26
+ *
27
+ * `PROTECTED_TABLES` is both, spliced into both route families and the
28
+ * restricted-caller SQL check, so no reader can disagree about what is
29
+ * private.
30
+ *
31
+ * `WRITE_PROTECTED_TABLES` decide how much is given away but must stay readable:
32
+ * the storefront provider loads the active automatic rules to price the order
33
+ * summary. A browser that could update `usage_count` or `discount_value` could
34
+ * discount forever, so writes need the same trust.
35
+ */
36
+ export const MONEY_TABLES: ReadonlyArray<string> = [
37
+ 'teleport_gift_cards',
38
+ 'teleport_gift_card_transactions',
39
+ 'teleport_vouchers',
40
+ 'teleport_voucher_redemptions',
41
+ // The sent-email log keeps the RENDERED message, and a gift-card email
42
+ // carries the full code — protecting the card table while serving its
43
+ // codes from the email log would be no protection at all.
44
+ 'teleport_sent_emails',
45
+ // A live reset token IS the account: whoever reads one sets the password.
46
+ // Only the server-side reset workflows ever read or write the table.
47
+ 'password_reset_tokens',
48
+ // A digital product's files and license keys ARE the product: the order
49
+ // page's server segment mints a download link per paid line, and the
50
+ // download route assigns a key; nothing else reads them.
51
+ 'teleport_product_files',
52
+ 'teleport_license_keys',
53
+ ]
54
+
55
+ export const CUSTOMER_RECORD_TABLES: ReadonlyArray<string> = [
56
+ 'users',
57
+ 'teleport_orders',
58
+ 'teleport_order_items',
59
+ 'teleport_invoices',
60
+ 'teleport_invoice_items',
61
+ 'teleport_events',
62
+ 'teleport_shipments',
63
+ 'teleport_shipment_items',
64
+ 'teleport_withdrawal_requests',
65
+ 'teleport_withdrawal_request_items',
66
+ 'teleport_abandoned_carts',
67
+ 'teleport_cart',
68
+ 'teleport_cart_items',
69
+ 'teleport_favourites',
70
+ // Who subscribed to what and what they downloaded: the buyer's own page reads
71
+ // them through a server segment, the admin through its session.
72
+ 'teleport_subscriptions',
73
+ 'teleport_downloads',
74
+ // A review is public once approved, but the row carries the reviewer's
75
+ // email and the ones still in moderation; the product page reads the
76
+ // approved ones server-side.
77
+ 'teleport_product_reviews',
78
+ // A push subscription is a capability: whoever holds the endpoint and its
79
+ // keys can notify that device. Only the app's push routes read the table.
80
+ 'teleport_push_subscriptions',
81
+ // A comment is public once approved, but the row carries the commenter's
82
+ // email and the comments still waiting for approval; the post page reads
83
+ // the approved ones server-side, public columns only.
84
+ 'teleport_blog_comments',
85
+ // What visitors told the AI assistant, and the merchant's private knowledge
86
+ // base it answers from: the assistant reads them in its own server
87
+ // segments, the admin lists them with its session.
88
+ 'teleport_ai_chat_knowledge_sources',
89
+ 'teleport_ai_chat_documents',
90
+ 'teleport_ai_chat_conversations',
91
+ 'teleport_ai_chat_messages',
92
+ ]
93
+
94
+ export const PROTECTED_TABLES: ReadonlyArray<string> = [...MONEY_TABLES, ...CUSTOMER_RECORD_TABLES]
95
+
96
+ export const WRITE_PROTECTED_TABLES: ReadonlyArray<string> = ['teleport_discounts']
97
+
98
+ /**
99
+ * The auth users table's CREDENTIALS — the password hash and the OAuth
100
+ * provider's tokens (an access or refresh token IS the user's account at the
101
+ * provider). Profile columns stay readable; these never leave the server in a
102
+ * row, whoever asks: the generated admin's users list shows no password either.
103
+ */
104
+ export const AUTH_CREDENTIAL_COLUMNS: ReadonlyArray<string> = [
105
+ 'password',
106
+ 'access_token',
107
+ 'refresh_token',
108
+ 'id_token',
109
+ 'session_state',
110
+ 'token_type',
111
+ 'scope',
112
+ 'expires_at',
113
+ ]
114
+
115
+ /** The table the generated auth reads and writes those credentials in. */
116
+ export const AUTH_CREDENTIAL_TABLES: ReadonlyArray<string> = ['users']
117
+
118
+ /**
119
+ * The credential names no other table has a reason to use: dropped from the
120
+ * rows of a raw statement too, where a column cannot be traced to its table
121
+ * (`scope` and `expires_at` are ordinary columns elsewhere — a gift card
122
+ * expires).
123
+ */
124
+ const UNAMBIGUOUS_CREDENTIAL_COLUMNS = AUTH_CREDENTIAL_COLUMNS.filter(
125
+ (column) => column !== 'scope' && column !== 'expires_at'
126
+ )
127
+
128
+ export interface PublicTableFeed {
129
+ /** The only columns a browser is ever served. */
130
+ columns: ReadonlyArray<string>
131
+ /** SQL every browser read of the table is narrowed with. */
132
+ predicate: string
133
+ }
134
+
135
+ /**
136
+ * A table a browser may read ONLY as a fixed projection of the rows the
137
+ * DATABASE considers live — never whole, never through raw SQL, and never
138
+ * filtered or sorted by a column outside the projection (a filter is an
139
+ * oracle: `usage_count > 100` answers for a column that is never returned).
140
+ *
141
+ * `teleport_discounts` is the merchant's campaign plan: drafts, paused rules,
142
+ * launches scheduled for next month, how often each has been used. The
143
+ * storefront needs none of that; it prices the order summary from the rules
144
+ * that apply right now. Serving exactly those rows is what keeps an
145
+ * unannounced campaign unannounced, and it makes the WINDOW the database's
146
+ * judgement, the same clock that re-prices the order at submit: the storefront
147
+ * prices every row it is served and re-derives nothing from the browser clock,
148
+ * so a disagreement with the server's re-pricing is resolved by re-reading the
149
+ * feed.
150
+ */
151
+ export const PUBLIC_FEED_TABLES: Readonly<Record<string, PublicTableFeed>> = {
152
+ teleport_discounts: {
153
+ // Exactly what the shared engine normalises a rule from (`__deNormalizeRule`),
154
+ // plus `status`, which the storefront still checks. The window bounds,
155
+ // `description` and `usage_count` are the merchant's, and stay with the
156
+ // merchant.
157
+ columns: [
158
+ 'id',
159
+ 'name',
160
+ 'status',
161
+ 'priority',
162
+ 'discount_type',
163
+ 'discount_value',
164
+ 'applies_to_all_products',
165
+ 'product_ids',
166
+ 'category_ids',
167
+ 'min_order_value',
168
+ 'min_quantity',
169
+ 'first_order_only',
170
+ 'buy_quantity',
171
+ 'get_quantity',
172
+ 'stackable',
173
+ ],
174
+ predicate:
175
+ "status = 'active' AND (starts_at IS NULL OR starts_at <= NOW()) AND (ends_at IS NULL OR ends_at > NOW())",
176
+ },
177
+ }
178
+
179
+ /** The generated back office: its folder in the pages tree and its route prefix. */
180
+ const ADMIN_FOLDER_NAME = 'admin-panel'
181
+ const ADMIN_ROUTE_PREFIX = '/admin'
182
+
183
+ /**
184
+ * The roles allowed to read a protected table from a browser: whoever may open
185
+ * the generated admin panel, read off the protection the UIDL records for its
186
+ * folder and pages. A project without an admin panel has no page that binds a
187
+ * money table, so the empty list (server-side callers only) is the right answer.
188
+ */
189
+ export const resolveTrustedReaderRoles = (auth?: UIDLAuthentication | null): string[] => {
190
+ const roles = new Set<string>()
191
+ if (!auth) {
192
+ return []
193
+ }
194
+ Object.values(auth.folderProtection || {}).forEach((folder) => {
195
+ if (folder && folder.folderName === ADMIN_FOLDER_NAME) {
196
+ ;(folder.allowedRoles || []).forEach((role) => roles.add(role))
197
+ }
198
+ })
199
+ Object.values(auth.pageProtection || {}).forEach((page) => {
200
+ const route = page && typeof page.route === 'string' ? page.route : ''
201
+ if (route === ADMIN_ROUTE_PREFIX || route.startsWith(`${ADMIN_ROUTE_PREFIX}/`)) {
202
+ ;(page.allowedRoles || []).forEach((role) => roles.add(role))
203
+ }
204
+ })
205
+ return Array.from(roles).sort()
206
+ }
207
+
208
+ /**
209
+ * ES5 source of the guard, inlined into a generated route. Declares:
210
+ * - `__taIsInternalRequest(req)` — the caller presented the app secret, or
211
+ * is the module's own server-side `fetchData` (`req.__tqServerCall`).
212
+ * - `__taIsRestrictedRequest(req)` — a server-side node that presents the app
213
+ * secret but runs SQL it did not write (the AI data-query node): trusted to
214
+ * run raw SQL, never trusted with the protected tables.
215
+ * - `__taBrowserIsReadOnly(req)` — true for a browser in an app that has an
216
+ * app secret: every server-side caller identifies itself there, so a browser
217
+ * reads through the structured routes only — no raw SQL, no writes.
218
+ * - `__taNormalizeTable(name)` — the bare table a spelling names.
219
+ * - `__taSqlMentionsTable(sql, table)` — the statement names the table as an
220
+ * identifier (bare, double-quoted or schema-qualified; comments and string
221
+ * literals cannot hide or fake a mention).
222
+ * - `__taAssertSqlTextAllowed(sql)` — throws a 403 error when an untrusted
223
+ * caller's raw statement touches a protected table or a feed table, or
224
+ * writes a write-protected one.
225
+ * - `__taPublicFeed(req, tableName)` — the projection an untrusted caller is
226
+ * served of a feed table (`{ columns, predicate }`), or `null` when the
227
+ * caller may read the table as asked (a server-side one) or the table has
228
+ * no feed.
229
+ * - `__taAssertFeedFields(tableName, feed, fields)` — throws a 403 error when
230
+ * a filter or sort field is not one of the feed's columns.
231
+ * - `__taWithoutCredentials(rows, tableName)` — the rows with the auth users
232
+ * table's credential columns removed: all of them for that table's rows,
233
+ * the unambiguous ones for a raw statement's (`tableName` null).
234
+ * - `__taIsCredentialTable(tableName)` — the table holds those credentials.
235
+ * - `__taGuardRead(req, tableName, rawQuery)` — for the per-table read routes:
236
+ * resolves to `null` when the request may proceed, else `{status, message}`.
237
+ * A protected table is readable by an internal caller or by a session whose
238
+ * role is in `trustedReaderRoles`; the session is decoded by the
239
+ * `__tqSessionToken` resolver, which the host route must inline as well.
240
+ */
241
+ export const generateTableAccessHelperCode = (params: {
242
+ trustedReaderRoles: ReadonlyArray<string>
243
+ }): string => {
244
+ return `// GENERATED — see generateTableAccessHelperCode in
245
+ // @teleporthq/teleport-shared/src/utils/table-access.ts.
246
+ //
247
+ // Tables that ARE money, and tables that decide how much of it is given away.
248
+ // A browser reads the first group only as a trusted role (the generated admin)
249
+ // and writes the second group never; server-side workflow segments present the
250
+ // app secret instead. One list for both data-route families.
251
+ var __TA_PROTECTED_TABLES = ${JSON.stringify(PROTECTED_TABLES)};
252
+ var __TA_WRITE_PROTECTED_TABLES = ${JSON.stringify(WRITE_PROTECTED_TABLES)};
253
+ // Readable from a browser only as this projection of the rows the database
254
+ // calls live — see PUBLIC_FEED_TABLES. The per-table routes serve a whole
255
+ // table (and raw SQL), which cannot express a feed, so there they are refused
256
+ // like a protected table.
257
+ var __TA_PUBLIC_FEED_TABLES = ${JSON.stringify(PUBLIC_FEED_TABLES)};
258
+ var __TA_FEED_TABLES = Object.keys(__TA_PUBLIC_FEED_TABLES);
259
+ var __TA_TRUSTED_READER_ROLES = ${JSON.stringify(params.trustedReaderRoles)};
260
+ var __TA_CREDENTIAL_COLUMNS = ${JSON.stringify(AUTH_CREDENTIAL_COLUMNS)};
261
+ var __TA_UNAMBIGUOUS_CREDENTIAL_COLUMNS = ${JSON.stringify(UNAMBIGUOUS_CREDENTIAL_COLUMNS)};
262
+ var __TA_CREDENTIAL_TABLES = ${JSON.stringify(AUTH_CREDENTIAL_TABLES)};
263
+ var __TA_SQL_WRITE_RE = /\\b(?:insert|update|delete|merge|truncate)\\b/i;
264
+ // What a statement can reach without spelling a table's name where this scan
265
+ // can see it: a Unicode-escaped identifier (U&"\\0075sers" is \`users\`), the
266
+ // functions that run SQL text or dump whole tables, schemas or the database
267
+ // (the table name then sits in a string, or is not named at all), and the
268
+ // planner statistics, which hold sampled column values of every table.
269
+ var __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;
270
+
271
+ // Only server code can read NEXTAUTH_SECRET, so a browser cannot forge the
272
+ // header a server segment sends. An in-process call (getStaticProps and the
273
+ // page-load workflows call the handler directly with a synthetic request)
274
+ // marks its request object instead — a property, never a header, so nothing
275
+ // on the wire can claim it.
276
+ function __taPresentsAppSecret(req) {
277
+ var secret = req && req.headers && req.headers['x-internal-data-secret'];
278
+ return !!(secret && process.env.NEXTAUTH_SECRET && secret === process.env.NEXTAUTH_SECRET);
279
+ }
280
+
281
+ function __taIsRestrictedRequest(req) {
282
+ return __taPresentsAppSecret(req) && !!(req.headers && req.headers['x-tq-restricted-sql'] === '1');
283
+ }
284
+
285
+ function __taIsInternalRequest(req) {
286
+ if (req && req.__tqServerCall === true) return true;
287
+ return __taPresentsAppSecret(req) && !__taIsRestrictedRequest(req);
288
+ }
289
+
290
+ // Generated browser code reads through the structured routes and never sends
291
+ // SQL or a write of its own — every data node runs server-side and presents
292
+ // the secret. So where a secret exists to tell the two apart, anything else a
293
+ // browser sends is refused whole: no statement scan has to be right about it.
294
+ // An app without one (no authentication, hence no store) keeps the scan.
295
+ function __taBrowserIsReadOnly(req) {
296
+ return !!process.env.NEXTAUTH_SECRET && !__taIsInternalRequest(req) && !__taIsRestrictedRequest(req);
297
+ }
298
+
299
+ // Comments and string literals out, so a table name inside a string neither
300
+ // hides a reference nor fakes one. Quoted identifiers stay, whole: in Postgres
301
+ // they name tables, which is exactly what is being looked for.
302
+ //
303
+ // ⛔ This is a lexer, not a search: a quote or comment marker inside a quoted
304
+ // identifier ("x'", "--") is part of the name, a \`$\` or an \`E\` inside an
305
+ // identifier (a$b$, ELSE'…') starts neither a dollar quote nor an escape
306
+ // string, and a dollar-quoted body is consumed by its own tag. An UNTERMINATED
307
+ // literal, identifier, comment or dollar quote makes the whole scan fail closed
308
+ // (ok:false): a statement this cleaner cannot read is one it cannot vouch for.
309
+ function __taCleanSql(sql) {
310
+ var text = String(sql || '');
311
+ var out = '';
312
+ var i = 0;
313
+ function inIdentifier(at) { return at > 0 && /[A-Za-z0-9_$]/.test(text[at - 1]); }
314
+ while (i < text.length) {
315
+ var ch = text[i];
316
+ var next = text[i + 1];
317
+ if (ch === '-' && next === '-') {
318
+ while (i < text.length && text[i] !== '\\n') i++;
319
+ continue;
320
+ }
321
+ if (ch === '/' && next === '*') {
322
+ i += 2;
323
+ var depth = 1;
324
+ while (i < text.length && depth > 0) {
325
+ if (text[i] === '/' && text[i + 1] === '*') { depth++; i += 2; continue; }
326
+ if (text[i] === '*' && text[i + 1] === '/') { depth--; i += 2; continue; }
327
+ i++;
328
+ }
329
+ if (depth > 0) return { ok: false, text: out };
330
+ out += ' ';
331
+ continue;
332
+ }
333
+ // "…" is one identifier token; "" inside it is an escaped quote.
334
+ if (ch === '"') {
335
+ var end = i + 1;
336
+ var closedIdentifier = false;
337
+ while (end < text.length) {
338
+ if (text[end] === '"' && text[end + 1] === '"') { end += 2; continue; }
339
+ if (text[end] === '"') { closedIdentifier = true; break; }
340
+ end++;
341
+ }
342
+ if (!closedIdentifier) return { ok: false, text: out };
343
+ out += text.slice(i, end + 1);
344
+ i = end + 1;
345
+ continue;
346
+ }
347
+ // $tag$ … $tag$ — the body is data whatever it holds.
348
+ if (ch === '$' && !inIdentifier(i)) {
349
+ var tagMatch = /^\\$[A-Za-z_][A-Za-z0-9_]*\\$|^\\$\\$/.exec(text.slice(i));
350
+ if (tagMatch) {
351
+ var tag = tagMatch[0];
352
+ var close = text.indexOf(tag, i + tag.length);
353
+ if (close === -1) return { ok: false, text: out };
354
+ i = close + tag.length;
355
+ out += ' ';
356
+ continue;
357
+ }
358
+ }
359
+ // E'…' understands backslash escapes; a plain '…' doubles the quote.
360
+ if (ch === "'" || ((ch === 'E' || ch === 'e') && next === "'" && !inIdentifier(i))) {
361
+ var escapes = ch !== "'";
362
+ if (escapes) i++;
363
+ i++;
364
+ var closed = false;
365
+ while (i < text.length) {
366
+ if (escapes && text[i] === '\\\\') { i += 2; continue; }
367
+ if (text[i] === "'" && text[i + 1] === "'") { i += 2; continue; }
368
+ if (text[i] === "'") { i++; closed = true; break; }
369
+ i++;
370
+ }
371
+ if (!closed) return { ok: false, text: out };
372
+ out += ' ';
373
+ continue;
374
+ }
375
+ out += ch;
376
+ i++;
377
+ }
378
+ return { ok: true, text: out };
379
+ }
380
+
381
+ function __taNormalizeTable(tableName) {
382
+ var name = String(tableName || '').trim().toLowerCase();
383
+ var parts = name.split('.');
384
+ return parts[parts.length - 1].replace(/"/g, '');
385
+ }
386
+
387
+ // An unreadable statement, or one that can reach a table without naming it,
388
+ // counts as mentioning EVERY protected table: fail closed rather than let a
389
+ // spelling this scan cannot see decide it is harmless.
390
+ function __taSqlMentionsTable(sql, table) {
391
+ var cleaned = __taCleanSql(sql);
392
+ if (!cleaned.ok || __TA_OPAQUE_SQL_RE.test(cleaned.text)) return true;
393
+ var pattern = new RegExp('(?:^|[^A-Za-z0-9_])"?' + table + '"?(?![A-Za-z0-9_])', 'i');
394
+ return pattern.test(cleaned.text);
395
+ }
396
+
397
+ function __taIsCredentialTable(tableName) {
398
+ return __TA_CREDENTIAL_TABLES.indexOf(__taNormalizeTable(tableName)) !== -1;
399
+ }
400
+
401
+ // Credentials never travel in a row: a column is dropped by name, however the
402
+ // caller spelled it in its projection.
403
+ function __taWithoutCredentials(rows, tableName) {
404
+ if (!Array.isArray(rows)) return rows;
405
+ var dropped = tableName && __taIsCredentialTable(tableName)
406
+ ? __TA_CREDENTIAL_COLUMNS
407
+ : tableName ? [] : __TA_UNAMBIGUOUS_CREDENTIAL_COLUMNS;
408
+ if (dropped.length === 0) return rows;
409
+ return rows.map(function(row) {
410
+ if (!row || typeof row !== 'object') return row;
411
+ var copy = {};
412
+ Object.keys(row).forEach(function(key) {
413
+ if (dropped.indexOf(String(key).toLowerCase()) === -1) copy[key] = row[key];
414
+ });
415
+ return copy;
416
+ });
417
+ }
418
+
419
+ function __taForbid(table) {
420
+ var err = new Error('Forbidden: ' + table + ' is only accessible to server-side workflow nodes');
421
+ err.status = 403;
422
+ throw err;
423
+ }
424
+
425
+ function __taAssertSqlTextAllowed(sql) {
426
+ var text = String(sql || '');
427
+ // A statement can hand back a whole row (\`SELECT u FROM users u\`,
428
+ // \`row_to_json(u)\`) without naming a credential column, so the table itself
429
+ // is out of reach of anything but the server's own calls.
430
+ for (var ci = 0; ci < __TA_CREDENTIAL_TABLES.length; ci++) {
431
+ if (__taSqlMentionsTable(text, __TA_CREDENTIAL_TABLES[ci])) __taForbid(__TA_CREDENTIAL_TABLES[ci]);
432
+ }
433
+ for (var pi = 0; pi < __TA_PROTECTED_TABLES.length; pi++) {
434
+ if (__taSqlMentionsTable(text, __TA_PROTECTED_TABLES[pi])) __taForbid(__TA_PROTECTED_TABLES[pi]);
435
+ }
436
+ // A feed table has exactly one browser-readable shape, and a statement the
437
+ // caller wrote is not it.
438
+ for (var fi = 0; fi < __TA_FEED_TABLES.length; fi++) {
439
+ if (__taSqlMentionsTable(text, __TA_FEED_TABLES[fi])) __taForbid(__TA_FEED_TABLES[fi]);
440
+ }
441
+ var cleaned = __taCleanSql(text);
442
+ // Unreadable, or a write: either way the write-protected tables are refused.
443
+ if (cleaned.ok && !__TA_SQL_WRITE_RE.test(cleaned.text)) return;
444
+ for (var wi = 0; wi < __TA_WRITE_PROTECTED_TABLES.length; wi++) {
445
+ if (__taSqlMentionsTable(text, __TA_WRITE_PROTECTED_TABLES[wi])) __taForbid(__TA_WRITE_PROTECTED_TABLES[wi]);
446
+ }
447
+ }
448
+
449
+ function __taRoleOf(token) {
450
+ if (!token || typeof token !== 'object') return null;
451
+ if (typeof token.role === 'string') return token.role;
452
+ if (typeof token.roleName === 'string') return token.roleName;
453
+ if (Array.isArray(token.roles) && typeof token.roles[0] === 'string') return token.roles[0];
454
+ return null;
455
+ }
456
+
457
+ // The tables a read of \`tableName\` (or the raw statement) touches that this
458
+ // route family may not serve to a browser: the protected ones, and the feed
459
+ // tables, whose one browser-readable projection lives on the data API instead.
460
+ var __TA_ROUTE_PROTECTED_TABLES = __TA_PROTECTED_TABLES.concat(__TA_FEED_TABLES);
461
+
462
+ function __taProtectedTablesRead(tableName, rawQuery) {
463
+ var touched = [];
464
+ var bare = __taNormalizeTable(tableName);
465
+ if (__TA_ROUTE_PROTECTED_TABLES.indexOf(bare) !== -1) touched.push(bare);
466
+ if (typeof rawQuery === 'string' && rawQuery.length > 0) {
467
+ for (var i = 0; i < __TA_ROUTE_PROTECTED_TABLES.length; i++) {
468
+ if (touched.indexOf(__TA_ROUTE_PROTECTED_TABLES[i]) === -1 && __taSqlMentionsTable(rawQuery, __TA_ROUTE_PROTECTED_TABLES[i])) {
469
+ touched.push(__TA_ROUTE_PROTECTED_TABLES[i]);
470
+ }
471
+ }
472
+ }
473
+ return touched;
474
+ }
475
+
476
+ // The projection an untrusted caller gets of a feed table, or null when the
477
+ // table has no feed or the caller is a server-side one reading it whole.
478
+ function __taPublicFeed(req, tableName) {
479
+ if (__taIsInternalRequest(req)) return null;
480
+ var feed = __TA_PUBLIC_FEED_TABLES[__taNormalizeTable(tableName)];
481
+ return feed || null;
482
+ }
483
+
484
+ // A feed is filtered and sorted only by the columns it serves: a condition on
485
+ // any other column (or an expression over one) would answer for it.
486
+ function __taAssertFeedFields(tableName, feed, fields) {
487
+ for (var i = 0; i < fields.length; i++) {
488
+ if (!fields[i]) continue;
489
+ var column = String(fields[i]).trim().replace(/"/g, '').toLowerCase();
490
+ if (feed.columns.indexOf(column) === -1) __taForbid(__taNormalizeTable(tableName) + '.' + fields[i]);
491
+ }
492
+ }
493
+
494
+ // null when the read may proceed, else { status, message }.
495
+ async function __taGuardRead(req, tableName, rawQuery) {
496
+ if (typeof rawQuery === 'string' && rawQuery.length > 0 && __taBrowserIsReadOnly(req)) {
497
+ return { status: 403, message: 'Forbidden: raw queries are only available to server-side calls' };
498
+ }
499
+ var touched = __taProtectedTablesRead(tableName, rawQuery);
500
+ if (touched.length === 0) return null;
501
+ if (__taIsInternalRequest(req)) return null;
502
+ var token = null;
503
+ try {
504
+ token = await __tqSessionToken(req);
505
+ } catch (e) {
506
+ token = null;
507
+ }
508
+ if (!token) return { status: 401, message: 'Unauthenticated' };
509
+ var role = __taRoleOf(token);
510
+ if (!role || __TA_TRUSTED_READER_ROLES.indexOf(role) === -1) {
511
+ return { status: 403, message: 'Forbidden: ' + touched[0] + ' is only readable by the store administrators' };
512
+ }
513
+ return null;
514
+ }
515
+ `
516
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The names the published app's service worker, its push routes and the
3
+ * workflow nodes that use them have to agree on. The node handlers are
4
+ * serialized into the generated app as source text, so they interpolate these
5
+ * values at generation time rather than importing them.
6
+ */
7
+
8
+ /** The browsers that agreed to receive the app's push notifications, in the project database. */
9
+ export const SUBSCRIPTIONS_TABLE = 'teleport_push_subscriptions'
10
+
11
+ /** Public: a browser saves (POST) or forgets (DELETE) its own subscription here. */
12
+ export const SUBSCRIPTIONS_ROUTE = '/api/push/subscriptions'
13
+
14
+ /** Internal: only the app's own server code may send a notification through it. */
15
+ export const SEND_ROUTE = '/api/push/send'
16
+
17
+ /** What a visitor-facing push step reports when the project has no push keys or no worker. */
18
+ export const NOT_CONFIGURED_MESSAGE = 'Push notifications are not set up for this site.'
19
+
20
+ export const PUBLIC_KEY_ENV = 'WEB_PUSH_VAPID_PUBLIC_KEY'
21
+
22
+ /** Filled at deploy from the project secret of the same name. */
23
+ export const PRIVATE_KEY_ENV = 'WEB_PUSH_VAPID_PRIVATE_KEY'
24
+
25
+ /**
26
+ * The send route accepts a request only when this header carries an HMAC of
27
+ * `SEND_TOKEN_PURPOSE` keyed with the VAPID private key — a value only the
28
+ * app's own server holds, so no extra secret has to be provisioned.
29
+ */
30
+ export const SEND_TOKEN_HEADER = 'x-teleport-push-token'
31
+ export const SEND_TOKEN_PURPOSE = 'teleport-web-push-send'
32
+
33
+ /**
34
+ * The page-side API the generated service-worker module publishes on `window`.
35
+ * A workflow node handler has no module scope, so a global is the only way it
36
+ * can reach the worker registration.
37
+ */
38
+ export const SERVICE_WORKER_GLOBAL = '__teleportServiceWorker'
39
+
40
+ export const SUBSCRIBE_NODE_TYPE = 'browser-subscribe-to-push'
41
+ export const UNSUBSCRIBE_NODE_TYPE = 'browser-unsubscribe-from-push'
42
+ export const GET_SUBSCRIPTION_NODE_TYPE = 'browser-get-push-subscription'
43
+ export const SHOW_NOTIFICATION_NODE_TYPE = 'browser-show-notification'
44
+ export const SEND_NODE_TYPE = 'push-send-notification'
45
+
46
+ /** Nodes that manage the visitor's push subscription and therefore need the push routes. */
47
+ export const PUSH_NODE_TYPES: ReadonlyArray<string> = [
48
+ SUBSCRIBE_NODE_TYPE,
49
+ UNSUBSCRIBE_NODE_TYPE,
50
+ GET_SUBSCRIPTION_NODE_TYPE,
51
+ SEND_NODE_TYPE,
52
+ ]
53
+
54
+ /**
55
+ * Nodes that need a service worker on the page: push itself, and on-page
56
+ * notifications, which Chrome on Android only shows through a worker.
57
+ */
58
+ export const SERVICE_WORKER_NODE_TYPES: ReadonlyArray<string> = [
59
+ ...PUSH_NODE_TYPES,
60
+ SHOW_NOTIFICATION_NODE_TYPE,
61
+ ]