@open-mercato/shared 0.9.1-develop.7574.1.837288eec1 → 0.10.1-develop.7576.1.21d5b50435
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +4 -4
- package/dist/lib/auth/server.js +1 -1
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/bootstrap/dynamicLoader.js +6 -46
- package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
- package/dist/lib/bootstrap/factory.js +3 -4
- package/dist/lib/bootstrap/factory.js.map +2 -2
- package/dist/lib/catalog-visibility/index.js +0 -4
- package/dist/lib/catalog-visibility/index.js.map +2 -2
- package/dist/lib/commands/command-bus.js +13 -45
- package/dist/lib/commands/command-bus.js.map +2 -2
- package/dist/lib/commands/types.js.map +2 -2
- package/dist/lib/crud/exporters.js +1 -1
- package/dist/lib/crud/exporters.js.map +2 -2
- package/dist/lib/crud/ids.js +1 -13
- package/dist/lib/crud/ids.js.map +2 -2
- package/dist/lib/db/pg-errors.js +0 -14
- package/dist/lib/db/pg-errors.js.map +2 -2
- package/dist/lib/localization/index.js +1 -9
- package/dist/lib/localization/index.js.map +2 -2
- package/dist/lib/localization/translatable-fields.js +0 -26
- package/dist/lib/localization/translatable-fields.js.map +2 -2
- package/dist/lib/number.js +0 -6
- package/dist/lib/number.js.map +2 -2
- package/dist/lib/openapi/generator.js +67 -84
- package/dist/lib/openapi/generator.js.map +2 -2
- package/dist/lib/query/engine.js +10 -45
- package/dist/lib/query/engine.js.map +2 -2
- package/dist/lib/query/join-utils.js +1 -1
- package/dist/lib/query/join-utils.js.map +2 -2
- package/dist/lib/query/types.js.map +2 -2
- package/dist/lib/url.js +0 -24
- package/dist/lib/url.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/lib/webhooks/body.js +3 -7
- package/dist/lib/webhooks/body.js.map +2 -2
- package/dist/lib/webhooks/index.js +0 -2
- package/dist/lib/webhooks/index.js.map +2 -2
- package/dist/modules/integrations/types.js.map +2 -2
- package/dist/modules/notifications/types.js +0 -4
- package/dist/modules/notifications/types.js.map +3 -3
- package/dist/modules/payment_gateways/types.js +1 -14
- package/dist/modules/payment_gateways/types.js.map +2 -2
- package/dist/modules/registry.js.map +2 -2
- package/dist/modules/search.js.map +2 -2
- package/dist/modules/widgets/injection-loader.js +3 -14
- package/dist/modules/widgets/injection-loader.js.map +2 -2
- package/package.json +3 -10
- package/src/lib/__tests__/number.test.ts +1 -24
- package/src/lib/auth/__tests__/server.test.ts +0 -14
- package/src/lib/auth/server.ts +1 -1
- package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.appModuleOverrides.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.cacheRecovery.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/factory.test.ts +6 -18
- package/src/lib/bootstrap/dynamicLoader.ts +5 -63
- package/src/lib/bootstrap/factory.ts +3 -8
- package/src/lib/bootstrap/types.ts +1 -5
- package/src/lib/catalog-visibility/index.ts +0 -2
- package/src/lib/commands/__tests__/command-bus.test.ts +1 -100
- package/src/lib/commands/command-bus.ts +10 -42
- package/src/lib/commands/command-interceptor.ts +0 -4
- package/src/lib/commands/types.ts +0 -14
- package/src/lib/crud/__tests__/exporters.test.ts +0 -18
- package/src/lib/crud/__tests__/ids.test.ts +1 -34
- package/src/lib/crud/exporters.ts +1 -1
- package/src/lib/crud/ids.ts +0 -24
- package/src/lib/db/__tests__/pg-errors.test.ts +1 -68
- package/src/lib/db/pg-errors.ts +0 -35
- package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +0 -23
- package/src/lib/localization/__tests__/translatable-fields.test.ts +0 -17
- package/src/lib/localization/index.ts +1 -8
- package/src/lib/localization/translatable-fields.ts +0 -46
- package/src/lib/number.ts +0 -13
- package/src/lib/openapi/generator.ts +77 -106
- package/src/lib/query/engine.ts +8 -58
- package/src/lib/query/join-utils.ts +1 -1
- package/src/lib/query/types.ts +1 -13
- package/src/lib/url.ts +0 -30
- package/src/lib/webhooks/__tests__/body.test.ts +0 -48
- package/src/lib/webhooks/body.ts +4 -11
- package/src/lib/webhooks/index.ts +0 -1
- package/src/modules/integrations/types.ts +0 -6
- package/src/modules/notifications/types.ts +0 -7
- package/src/modules/payment_gateways/__tests__/types.test.ts +0 -44
- package/src/modules/payment_gateways/types.ts +6 -52
- package/src/modules/registry.ts +0 -5
- package/src/modules/search.ts +0 -39
- package/src/modules/widgets/injection-loader.ts +3 -33
- package/dist/lib/bootstrap/next-subpath-resolve-hook-worker.js +0 -6
- package/dist/lib/bootstrap/next-subpath-resolve-hook-worker.js.map +0 -7
- package/dist/lib/bootstrap/next-subpath-resolve-hook.js +0 -62
- package/dist/lib/bootstrap/next-subpath-resolve-hook.js.map +0 -7
- package/dist/lib/catalog-visibility/canonicalizeEffectiveScope.js +0 -33
- package/dist/lib/catalog-visibility/canonicalizeEffectiveScope.js.map +0 -7
- package/dist/lib/catalog-visibility/hashEffectiveScope.js +0 -9
- package/dist/lib/catalog-visibility/hashEffectiveScope.js.map +0 -7
- package/dist/lib/db/advisoryLock.js +0 -172
- package/dist/lib/db/advisoryLock.js.map +0 -7
- package/dist/lib/query/overlap.js +0 -58
- package/dist/lib/query/overlap.js.map +0 -7
- package/dist/modules/notifications/safeHref.js +0 -20
- package/dist/modules/notifications/safeHref.js.map +0 -7
- package/src/lib/__tests__/url.cross-site.test.ts +0 -140
- package/src/lib/bootstrap/__tests__/dynamicLoader.externalSubpath.test.ts +0 -141
- package/src/lib/bootstrap/__tests__/next-subpath-resolve-hook.test.ts +0 -174
- package/src/lib/bootstrap/next-subpath-resolve-hook-worker.ts +0 -3
- package/src/lib/bootstrap/next-subpath-resolve-hook.ts +0 -130
- package/src/lib/catalog-visibility/__tests__/hashEffectiveScope.test.ts +0 -121
- package/src/lib/catalog-visibility/canonicalizeEffectiveScope.ts +0 -41
- package/src/lib/catalog-visibility/hashEffectiveScope.ts +0 -8
- package/src/lib/db/__tests__/advisoryLock.integration.test.ts +0 -343
- package/src/lib/db/__tests__/advisoryLock.test.ts +0 -564
- package/src/lib/db/advisoryLock.ts +0 -271
- package/src/lib/openapi/__tests__/optional-path-params.test.ts +0 -102
- package/src/lib/query/__tests__/empty-ids-filter.pg.test.ts +0 -89
- package/src/lib/query/__tests__/engine.empty-in-filter.test.ts +0 -79
- package/src/lib/query/__tests__/overlap.test.ts +0 -219
- package/src/lib/query/overlap.ts +0 -106
- package/src/modules/notifications/safeHref.ts +0 -19
- package/src/modules/widgets/__tests__/injection-loader.partitioned-registration.test.ts +0 -78
package/src/lib/query/engine.ts
CHANGED
|
@@ -38,13 +38,6 @@ import { resolveListCountCap } from './count-cap'
|
|
|
38
38
|
import { mapWithConcurrency } from './bounded-decrypt'
|
|
39
39
|
import { parseNumberWithDefault } from '../number'
|
|
40
40
|
import { createLogger } from '../logger'
|
|
41
|
-
import {
|
|
42
|
-
buildArrayColumnNoOverlapPredicate,
|
|
43
|
-
buildArrayColumnOverlapPredicate,
|
|
44
|
-
buildJsonbNoOverlapPredicate,
|
|
45
|
-
buildJsonbOverlapPredicate,
|
|
46
|
-
buildScalarOverlapPredicate,
|
|
47
|
-
} from './overlap'
|
|
48
41
|
|
|
49
42
|
const logger = createLogger('shared').child({ component: 'query' })
|
|
50
43
|
|
|
@@ -1223,11 +1216,7 @@ export class BasicQueryEngine implements QueryEngine {
|
|
|
1223
1216
|
})
|
|
1224
1217
|
continue
|
|
1225
1218
|
}
|
|
1226
|
-
q = f.op
|
|
1227
|
-
? q.where(buildScalarOverlapPredicate(expr, f.value))
|
|
1228
|
-
: f.op === 'noverlap'
|
|
1229
|
-
? q.where(sql<boolean>`false`)
|
|
1230
|
-
: this.applyColumnOp(q, expr, f.op, f.value)
|
|
1219
|
+
q = this.applyColumnOp(q, expr, f.op, f.value)
|
|
1231
1220
|
}
|
|
1232
1221
|
|
|
1233
1222
|
// OR groups are applied here, after the cf:* value expressions exist, so a
|
|
@@ -1274,10 +1263,7 @@ export class BasicQueryEngine implements QueryEngine {
|
|
|
1274
1263
|
value: rf.value,
|
|
1275
1264
|
})
|
|
1276
1265
|
}
|
|
1277
|
-
|
|
1278
|
-
return rf.op === 'overlap'
|
|
1279
|
-
? buildScalarOverlapPredicate(cfValueExprByKey[rf.key], rf.value)
|
|
1280
|
-
: this.buildColumnOpExpression(eb, cfValueExprByKey[rf.key], rf.op, rf.value)
|
|
1266
|
+
return this.buildColumnOpExpression(eb, cfValueExprByKey[rf.key], rf.op, rf.value)
|
|
1281
1267
|
}
|
|
1282
1268
|
return this.buildIndexDocOpExpression(eb, {
|
|
1283
1269
|
entity: String(entity),
|
|
@@ -1603,22 +1589,10 @@ export class BasicQueryEngine implements QueryEngine {
|
|
|
1603
1589
|
return builder.where(column as any, '<', value as any)
|
|
1604
1590
|
case 'lte':
|
|
1605
1591
|
return builder.where(column as any, '<=', value as any)
|
|
1606
|
-
case 'in':
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
const inValues = Array.isArray(value) ? value : [value]
|
|
1611
|
-
return inValues.length === 0
|
|
1612
|
-
? builder.where(sql<boolean>`false`)
|
|
1613
|
-
: builder.where(column as any, 'in', inValues)
|
|
1614
|
-
}
|
|
1615
|
-
case 'nin': {
|
|
1616
|
-
// Mirrors 'in' above: empty means "exclude nothing".
|
|
1617
|
-
const ninValues = Array.isArray(value) ? value : [value]
|
|
1618
|
-
return ninValues.length === 0
|
|
1619
|
-
? builder.where(sql<boolean>`true`)
|
|
1620
|
-
: builder.where(column as any, 'not in', ninValues)
|
|
1621
|
-
}
|
|
1592
|
+
case 'in':
|
|
1593
|
+
return builder.where(column as any, 'in', Array.isArray(value) ? value : [value])
|
|
1594
|
+
case 'nin':
|
|
1595
|
+
return builder.where(column as any, 'not in', Array.isArray(value) ? value : [value])
|
|
1622
1596
|
case 'like':
|
|
1623
1597
|
return builder.where(column as any, 'like', value as any)
|
|
1624
1598
|
case 'ilike':
|
|
@@ -1627,10 +1601,6 @@ export class BasicQueryEngine implements QueryEngine {
|
|
|
1627
1601
|
return value
|
|
1628
1602
|
? builder.where(column as any, 'is not', null)
|
|
1629
1603
|
: builder.where(column as any, 'is', null)
|
|
1630
|
-
case 'overlap':
|
|
1631
|
-
return builder.where(buildArrayColumnOverlapPredicate(column, value))
|
|
1632
|
-
case 'noverlap':
|
|
1633
|
-
return builder.where(buildArrayColumnNoOverlapPredicate(column, value))
|
|
1634
1604
|
default:
|
|
1635
1605
|
return builder
|
|
1636
1606
|
}
|
|
@@ -1764,11 +1734,6 @@ export class BasicQueryEngine implements QueryEngine {
|
|
|
1764
1734
|
case 'exists':
|
|
1765
1735
|
predicate = sql<boolean>`${caseExpr} is not null`
|
|
1766
1736
|
break
|
|
1767
|
-
case 'overlap':
|
|
1768
|
-
predicate = buildScalarOverlapPredicate(caseExpr, value)
|
|
1769
|
-
break
|
|
1770
|
-
case 'noverlap':
|
|
1771
|
-
return sql<boolean>`false`
|
|
1772
1737
|
default:
|
|
1773
1738
|
// Mirrors buildColumnOpExpression's unknown-op fallback: a neutral
|
|
1774
1739
|
// predicate, so full and count shapes drop the same leaves.
|
|
@@ -1786,20 +1751,11 @@ export class BasicQueryEngine implements QueryEngine {
|
|
|
1786
1751
|
case 'gte': return eb(column, '>=', value)
|
|
1787
1752
|
case 'lt': return eb(column, '<', value)
|
|
1788
1753
|
case 'lte': return eb(column, '<=', value)
|
|
1789
|
-
case 'in':
|
|
1790
|
-
|
|
1791
|
-
const inValues = Array.isArray(value) ? value : [value]
|
|
1792
|
-
return inValues.length === 0 ? eb.val(false) : eb(column, 'in', inValues)
|
|
1793
|
-
}
|
|
1794
|
-
case 'nin': {
|
|
1795
|
-
const ninValues = Array.isArray(value) ? value : [value]
|
|
1796
|
-
return ninValues.length === 0 ? eb.val(true) : eb(column, 'not in', ninValues)
|
|
1797
|
-
}
|
|
1754
|
+
case 'in': return eb(column, 'in', Array.isArray(value) ? value : [value])
|
|
1755
|
+
case 'nin': return eb(column, 'not in', Array.isArray(value) ? value : [value])
|
|
1798
1756
|
case 'like': return eb(column, 'like', value)
|
|
1799
1757
|
case 'ilike': return eb(column, 'ilike', value)
|
|
1800
1758
|
case 'exists': return value ? eb(column, 'is not', null) : eb(column, 'is', null)
|
|
1801
|
-
case 'overlap': return buildArrayColumnOverlapPredicate(column, value)
|
|
1802
|
-
case 'noverlap': return buildArrayColumnNoOverlapPredicate(column, value)
|
|
1803
1759
|
default: return eb.val(true)
|
|
1804
1760
|
}
|
|
1805
1761
|
}
|
|
@@ -2017,12 +1973,6 @@ export class BasicQueryEngine implements QueryEngine {
|
|
|
2017
1973
|
? sub.where(sql<boolean>`${textExpr} is not null`)
|
|
2018
1974
|
: sub.where(sql<boolean>`${textExpr} is null`)
|
|
2019
1975
|
break
|
|
2020
|
-
case 'overlap':
|
|
2021
|
-
sub = sub.where(buildJsonbOverlapPredicate(`${alias}.doc`, opts.field, opts.value))
|
|
2022
|
-
break
|
|
2023
|
-
case 'noverlap':
|
|
2024
|
-
sub = sub.where(buildJsonbNoOverlapPredicate(`${alias}.doc`, opts.field, opts.value))
|
|
2025
|
-
break
|
|
2026
1976
|
default:
|
|
2027
1977
|
break
|
|
2028
1978
|
}
|
|
@@ -7,7 +7,7 @@ type AnyBuilder = any
|
|
|
7
7
|
|
|
8
8
|
export type NormalizedFilter = { field: string; op: FilterOp; value?: unknown; orGroup?: string; qualified?: string | null }
|
|
9
9
|
|
|
10
|
-
const VALID_OPS = ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'like', 'ilike', 'exists'
|
|
10
|
+
const VALID_OPS = ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'like', 'ilike', 'exists'] as const
|
|
11
11
|
|
|
12
12
|
const normalizeField = (key: string) => (key.startsWith('cf_') ? `cf:${key.slice(3)}` : key)
|
|
13
13
|
|
package/src/lib/query/types.ts
CHANGED
|
@@ -2,17 +2,7 @@ import type { EntityId } from '@open-mercato/shared/modules/entities'
|
|
|
2
2
|
import type { Profiler } from '../profiler'
|
|
3
3
|
import type { ResolvedCustomFieldDefinitions } from '../crud/custom-field-definition-index'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
* `overlap` (additive, 2026-10-05): "has any of" over a multi-valued field. An index-document
|
|
7
|
-
* string array compiles to jsonb `?|`, a `cf:*` key to any-value-in-set, and an array-typed base
|
|
8
|
-
* column to `&&`. An empty value list matches nothing.
|
|
9
|
-
*
|
|
10
|
-
* `noverlap` (additive, 2026-10-05): "has none of" — the negation of `overlap` on an index-document
|
|
11
|
-
* string array or an array-typed base column. Only a present array can match (a missing/null key or
|
|
12
|
-
* NULL column never does); an empty value list excludes nothing. Unsupported on `cf:*` keys, where
|
|
13
|
-
* it compiles to `false`.
|
|
14
|
-
*/
|
|
15
|
-
export type FilterOp = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'like' | 'ilike' | 'exists' | 'overlap' | 'noverlap'
|
|
5
|
+
export type FilterOp = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'like' | 'ilike' | 'exists'
|
|
16
6
|
|
|
17
7
|
export enum SortDir {
|
|
18
8
|
Asc = 'asc',
|
|
@@ -44,8 +34,6 @@ export type WhereOps<T> = {
|
|
|
44
34
|
$like?: T extends string ? string : never
|
|
45
35
|
$ilike?: T extends string ? string : never
|
|
46
36
|
$exists?: boolean
|
|
47
|
-
$overlap?: T extends readonly (infer Element)[] ? Element[] : T[]
|
|
48
|
-
$noverlap?: T extends readonly (infer Element)[] ? Element[] : T[]
|
|
49
37
|
}
|
|
50
38
|
|
|
51
39
|
// A field filter can be a direct value (equals) or ops object
|
package/src/lib/url.ts
CHANGED
|
@@ -267,36 +267,6 @@ export function toSecurityEmailUrl(input: RequestInput, path: string, env: EnvLi
|
|
|
267
267
|
return `${base}/${path.replace(/^\/+/, '')}`
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
export function isCrossSiteBrowserRequest(req: Request, env: EnvLike = process.env): boolean {
|
|
271
|
-
const rawFetchSite = req.headers.get('sec-fetch-site')
|
|
272
|
-
const fetchSite = rawFetchSite?.trim().toLowerCase() ?? null
|
|
273
|
-
if (fetchSite === 'cross-site') return true
|
|
274
|
-
if (fetchSite === 'same-origin' || fetchSite === 'none') return false
|
|
275
|
-
|
|
276
|
-
const rawOrigin = req.headers.get('origin')
|
|
277
|
-
if (rawOrigin === null) return rawFetchSite !== null
|
|
278
|
-
const origin = normalizeOrigin(rawOrigin)
|
|
279
|
-
if (!origin) return true
|
|
280
|
-
|
|
281
|
-
const allowedOrigins = readAllowedOrigins(env)
|
|
282
|
-
if (allowedOrigins.has(origin)) return false
|
|
283
|
-
if (shouldAllowLoopbackOrigin(origin, allowedOrigins, env)) return false
|
|
284
|
-
|
|
285
|
-
const forwardedProto = readFirstHeaderValue(req.headers.get('x-forwarded-proto'))
|
|
286
|
-
const protocol = forwardedProto
|
|
287
|
-
? (forwardedProto.endsWith(':') ? forwardedProto : `${forwardedProto}:`)
|
|
288
|
-
: new URL(origin).protocol
|
|
289
|
-
const host = req.headers.get('host')
|
|
290
|
-
const forwardedHost = req.headers.get('x-forwarded-host')
|
|
291
|
-
if (originFromHost(protocol, forwardedHost) === origin) return false
|
|
292
|
-
if (originFromHost(protocol, host) === origin) return false
|
|
293
|
-
|
|
294
|
-
if (env.NODE_ENV !== 'test') {
|
|
295
|
-
logger.warn('Cross-site request rejected', { origin, host, forwardedHost })
|
|
296
|
-
}
|
|
297
|
-
return true
|
|
298
|
-
}
|
|
299
|
-
|
|
300
270
|
export function mapSecurityEmailUrlError(
|
|
301
271
|
error: unknown,
|
|
302
272
|
mapping: SecurityEmailUrlErrorMapping,
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import {
|
|
3
3
|
DEFAULT_WEBHOOK_BODY_LIMIT_BYTES,
|
|
4
4
|
readBoundedRequestBody,
|
|
5
|
-
readBoundedRequestBytes,
|
|
6
5
|
resolveWebhookBodyLimitBytes,
|
|
7
6
|
WebhookBodyTooLargeError,
|
|
8
7
|
} from '../body'
|
|
@@ -78,53 +77,6 @@ describe('readBoundedRequestBody', () => {
|
|
|
78
77
|
})
|
|
79
78
|
})
|
|
80
79
|
|
|
81
|
-
describe('readBoundedRequestBytes', () => {
|
|
82
|
-
it('preserves exact bytes including invalid UTF-8', async () => {
|
|
83
|
-
const bytes = new Uint8Array([0xff, 0xfe, 0x41, 0xc3, 0x28])
|
|
84
|
-
const { request } = makeStreamingRequest([bytes.slice(0, 2), bytes.slice(2)])
|
|
85
|
-
|
|
86
|
-
const result = await readBoundedRequestBytes(request, { maxBytes: bytes.byteLength })
|
|
87
|
-
|
|
88
|
-
expect(Array.from(result)).toEqual(Array.from(bytes))
|
|
89
|
-
expect(new TextDecoder().decode(result)).toContain('\uFFFD')
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it('returns an empty array for a missing body', async () => {
|
|
93
|
-
const request = new Request('http://localhost/webhook', { method: 'POST' })
|
|
94
|
-
|
|
95
|
-
expect((await readBoundedRequestBytes(request)).byteLength).toBe(0)
|
|
96
|
-
await expect(readBoundedRequestBody(request)).resolves.toBe('')
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('enforces the declared and streamed limits and cancels the stream', async () => {
|
|
100
|
-
const declared = new Request('http://localhost/webhook', {
|
|
101
|
-
method: 'POST',
|
|
102
|
-
headers: { 'content-length': '6' },
|
|
103
|
-
body: 'ok',
|
|
104
|
-
})
|
|
105
|
-
await expect(readBoundedRequestBytes(declared, { maxBytes: 5 })).rejects.toBeInstanceOf(
|
|
106
|
-
WebhookBodyTooLargeError,
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
const { request, cancel } = makeStreamingRequest([
|
|
110
|
-
new Uint8Array([1, 2, 3]),
|
|
111
|
-
new Uint8Array([4, 5, 6]),
|
|
112
|
-
new Uint8Array([7, 8, 9]),
|
|
113
|
-
])
|
|
114
|
-
await expect(readBoundedRequestBytes(request, { maxBytes: 5 })).rejects.toBeInstanceOf(
|
|
115
|
-
WebhookBodyTooLargeError,
|
|
116
|
-
)
|
|
117
|
-
expect(cancel).toHaveBeenCalled()
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
it('keeps the string reader decoding the same bytes', async () => {
|
|
121
|
-
const bytes = new TextEncoder().encode('zażółć')
|
|
122
|
-
const { request } = makeStreamingRequest([bytes])
|
|
123
|
-
|
|
124
|
-
await expect(readBoundedRequestBody(request)).resolves.toBe('zażółć')
|
|
125
|
-
})
|
|
126
|
-
})
|
|
127
|
-
|
|
128
80
|
describe('resolveWebhookBodyLimitBytes', () => {
|
|
129
81
|
it('uses the documented default for missing or invalid configuration', () => {
|
|
130
82
|
expect(resolveWebhookBodyLimitBytes(undefined)).toBe(DEFAULT_WEBHOOK_BODY_LIMIT_BYTES)
|
package/src/lib/webhooks/body.ts
CHANGED
|
@@ -23,10 +23,10 @@ export function resolveWebhookBodyLimitBytes(
|
|
|
23
23
|
return Number.isSafeInteger(parsed) ? parsed : fallback
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export async function
|
|
26
|
+
export async function readBoundedRequestBody(
|
|
27
27
|
request: Request,
|
|
28
28
|
options?: { maxBytes?: number },
|
|
29
|
-
): Promise<
|
|
29
|
+
): Promise<string> {
|
|
30
30
|
const configuredLimit = options?.maxBytes ?? resolveWebhookBodyLimitBytes()
|
|
31
31
|
const maxBytes = Number.isSafeInteger(configuredLimit) && configuredLimit > 0
|
|
32
32
|
? configuredLimit
|
|
@@ -39,7 +39,7 @@ export async function readBoundedRequestBytes(
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
if (!request.body) return
|
|
42
|
+
if (!request.body) return ''
|
|
43
43
|
|
|
44
44
|
const reader = request.body.getReader()
|
|
45
45
|
const chunks: Uint8Array[] = []
|
|
@@ -66,12 +66,5 @@ export async function readBoundedRequestBytes(
|
|
|
66
66
|
body.set(chunk, offset)
|
|
67
67
|
offset += chunk.byteLength
|
|
68
68
|
}
|
|
69
|
-
return body
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export async function readBoundedRequestBody(
|
|
73
|
-
request: Request,
|
|
74
|
-
options?: { maxBytes?: number },
|
|
75
|
-
): Promise<string> {
|
|
76
|
-
return new TextDecoder().decode(await readBoundedRequestBytes(request, options))
|
|
69
|
+
return new TextDecoder().decode(body)
|
|
77
70
|
}
|
|
@@ -157,12 +157,6 @@ export interface IntegrationDetailPageConfig {
|
|
|
157
157
|
* Provider modules can replace these with injected tabs using the same spot.
|
|
158
158
|
*/
|
|
159
159
|
hiddenTabs?: IntegrationDetailBuiltInTab[]
|
|
160
|
-
/**
|
|
161
|
-
* Detail-page tab that holds the provider's Connect / Reconnect controls (a built-in tab id or
|
|
162
|
-
* an injected tab id). The OAuth reauth banner links to `?tab=<connectTabId>` while that tab is
|
|
163
|
-
* visible and the viewer holds `integrations.credentials.manage`.
|
|
164
|
-
*/
|
|
165
|
-
connectTabId?: string
|
|
166
160
|
}
|
|
167
161
|
|
|
168
162
|
export type IntegrationDetailBuiltInTab =
|
|
@@ -3,13 +3,6 @@ import type { ComponentType } from 'react'
|
|
|
3
3
|
export type NotificationStatus = 'unread' | 'read' | 'actioned' | 'dismissed'
|
|
4
4
|
export type NotificationSeverity = 'info' | 'warning' | 'success' | 'error'
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* `code` of the 409 body `POST /api/notifications/:id/action` answers with when the notification's
|
|
8
|
-
* action has already been executed. Lets a client tell that refusal apart from a 409 raised by
|
|
9
|
-
* the action's own command.
|
|
10
|
-
*/
|
|
11
|
-
export const NOTIFICATION_ACTION_ALREADY_EXECUTED_ERROR_CODE = 'notification_action_already_executed'
|
|
12
|
-
|
|
13
6
|
export type NotificationAction = {
|
|
14
7
|
id: string
|
|
15
8
|
label: string
|
|
@@ -3,9 +3,6 @@ import {
|
|
|
3
3
|
getWebhookHandler,
|
|
4
4
|
registerWebhookHandler,
|
|
5
5
|
type VerifyWebhookInput,
|
|
6
|
-
type WebhookLocatorContext,
|
|
7
|
-
type WebhookResponseOutcome,
|
|
8
|
-
WebhookVerificationUnavailableError,
|
|
9
6
|
type WebhookEvent,
|
|
10
7
|
} from '../types'
|
|
11
8
|
|
|
@@ -37,45 +34,4 @@ describe('payment gateway webhook registration', () => {
|
|
|
37
34
|
expect(getWebhookHandler('invalid')).toBeUndefined()
|
|
38
35
|
},
|
|
39
36
|
)
|
|
40
|
-
|
|
41
|
-
it('stays compatible with one-argument locator hints', () => {
|
|
42
|
-
const readSessionIdHint = (payload: Record<string, unknown> | null) => (payload ? 'sess' : null)
|
|
43
|
-
registerWebhookHandler('legacy', handler, { readSessionIdHint })
|
|
44
|
-
|
|
45
|
-
expect(getWebhookHandler('legacy')?.readSessionIdHint?.({})).toBe('sess')
|
|
46
|
-
expect(getWebhookHandler('legacy')?.rawBody).toBeUndefined()
|
|
47
|
-
expect(getWebhookHandler('legacy')?.formatResponse).toBeUndefined()
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('passes through the optional extension hooks', () => {
|
|
51
|
-
const readSessionIdHint = (_payload: Record<string, unknown> | null, context?: WebhookLocatorContext) =>
|
|
52
|
-
context ? String(context.headers['x-id']) : null
|
|
53
|
-
const readPaymentIdHint = () => 'pay'
|
|
54
|
-
const formatResponse = (outcome: WebhookResponseOutcome) => ({ status: 200, body: outcome })
|
|
55
|
-
registerWebhookHandler('extended', handler, {
|
|
56
|
-
readSessionIdHint,
|
|
57
|
-
readPaymentIdHint,
|
|
58
|
-
rawBody: 'bytes',
|
|
59
|
-
formatResponse,
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
const registration = getWebhookHandler('extended')
|
|
63
|
-
expect(registration?.rawBody).toBe('bytes')
|
|
64
|
-
expect(registration?.readPaymentIdHint).toBe(readPaymentIdHint)
|
|
65
|
-
expect(registration?.formatResponse).toBe(formatResponse)
|
|
66
|
-
expect(registration?.readSessionIdHint?.(null, { rawBody: '', headers: { 'x-id': 'abc' } })).toBe('abc')
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it('rejects an unknown rawBody mode', () => {
|
|
70
|
-
expect(() =>
|
|
71
|
-
registerWebhookHandler('bad', handler, { rawBody: 'stream' as unknown as 'text' }),
|
|
72
|
-
).toThrow('[internal] Payment gateway webhook rawBody must be')
|
|
73
|
-
expect(getWebhookHandler('bad')).toBeUndefined()
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('exposes a typed verification-unavailable error', () => {
|
|
77
|
-
const error = new WebhookVerificationUnavailableError()
|
|
78
|
-
expect(error).toBeInstanceOf(Error)
|
|
79
|
-
expect(error.name).toBe('WebhookVerificationUnavailableError')
|
|
80
|
-
})
|
|
81
37
|
})
|
|
@@ -237,45 +237,6 @@ export interface VerifyWebhookInput {
|
|
|
237
237
|
rawBody: string | Buffer
|
|
238
238
|
headers: Record<string, string | string[] | undefined>
|
|
239
239
|
credentials: Record<string, unknown>
|
|
240
|
-
/** Platform-resolved payment the webhook is expected to concern, when a locator found one. */
|
|
241
|
-
candidate?: WebhookCandidateSnapshot
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/** Raw request material handed to webhook locator hints. */
|
|
245
|
-
export interface WebhookLocatorContext {
|
|
246
|
-
rawBody: string | Buffer
|
|
247
|
-
headers: Record<string, string | string[] | undefined>
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export interface WebhookCandidateSnapshot {
|
|
251
|
-
transactionId: string
|
|
252
|
-
paymentId: string
|
|
253
|
-
providerSessionId: string | null
|
|
254
|
-
amount: string
|
|
255
|
-
currencyCode: string
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
export type WebhookResponseOutcome =
|
|
259
|
-
| 'accepted'
|
|
260
|
-
| 'no_candidate'
|
|
261
|
-
| 'verification_failed'
|
|
262
|
-
| 'verification_unavailable'
|
|
263
|
-
| 'processing_failed'
|
|
264
|
-
| 'payload_too_large'
|
|
265
|
-
| 'rate_limited'
|
|
266
|
-
|
|
267
|
-
export interface WebhookHttpResponse {
|
|
268
|
-
status: number
|
|
269
|
-
body: string | Record<string, unknown>
|
|
270
|
-
contentType?: string
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/** Thrown by a webhook handler when verification cannot be performed (for example a provider outage). */
|
|
274
|
-
export class WebhookVerificationUnavailableError extends Error {
|
|
275
|
-
constructor(message = 'Webhook verification is unavailable') {
|
|
276
|
-
super(message)
|
|
277
|
-
this.name = 'WebhookVerificationUnavailableError'
|
|
278
|
-
}
|
|
279
240
|
}
|
|
280
241
|
|
|
281
242
|
export interface WebhookEvent {
|
|
@@ -291,15 +252,10 @@ export interface WebhookEvent {
|
|
|
291
252
|
export interface WebhookHandlerRegistration {
|
|
292
253
|
handler: (input: VerifyWebhookInput) => Promise<WebhookEvent>
|
|
293
254
|
queue?: string
|
|
294
|
-
readSessionIdHint?: (payload: Record<string, unknown> | null
|
|
295
|
-
readPaymentIdHint?: (payload: Record<string, unknown> | null, context?: WebhookLocatorContext) => string | null
|
|
255
|
+
readSessionIdHint?: (payload: Record<string, unknown> | null) => string | null
|
|
296
256
|
maxBodyBytes?: number
|
|
297
|
-
rawBody?: WebhookRawBodyMode
|
|
298
|
-
formatResponse?: (outcome: WebhookResponseOutcome) => WebhookHttpResponse
|
|
299
257
|
}
|
|
300
258
|
|
|
301
|
-
export type WebhookRawBodyMode = 'text' | 'bytes'
|
|
302
|
-
|
|
303
259
|
// ── Adapter Registry Options ────────────────────────────────────────────────
|
|
304
260
|
|
|
305
261
|
export interface RegisterAdapterOptions {
|
|
@@ -389,11 +345,12 @@ export function clearGatewayAdapters(): void {
|
|
|
389
345
|
export function registerWebhookHandler(
|
|
390
346
|
providerKey: string,
|
|
391
347
|
handler: (input: VerifyWebhookInput) => Promise<WebhookEvent>,
|
|
392
|
-
options?:
|
|
348
|
+
options?: {
|
|
349
|
+
queue?: string
|
|
350
|
+
readSessionIdHint?: (payload: Record<string, unknown> | null) => string | null
|
|
351
|
+
maxBodyBytes?: number
|
|
352
|
+
},
|
|
393
353
|
): () => void {
|
|
394
|
-
if (options?.rawBody !== undefined && options.rawBody !== 'text' && options.rawBody !== 'bytes') {
|
|
395
|
-
throw new Error("[internal] Payment gateway webhook rawBody must be 'text' or 'bytes'")
|
|
396
|
-
}
|
|
397
354
|
if (
|
|
398
355
|
options?.maxBodyBytes !== undefined
|
|
399
356
|
&& (!Number.isSafeInteger(options.maxBodyBytes)
|
|
@@ -409,10 +366,7 @@ export function registerWebhookHandler(
|
|
|
409
366
|
handler,
|
|
410
367
|
queue: options?.queue,
|
|
411
368
|
readSessionIdHint: options?.readSessionIdHint,
|
|
412
|
-
readPaymentIdHint: options?.readPaymentIdHint,
|
|
413
369
|
maxBodyBytes: options?.maxBodyBytes,
|
|
414
|
-
rawBody: options?.rawBody,
|
|
415
|
-
formatResponse: options?.formatResponse,
|
|
416
370
|
})
|
|
417
371
|
return () => {
|
|
418
372
|
webhookHandlerRegistry.delete(providerKey)
|
package/src/modules/registry.ts
CHANGED
|
@@ -178,11 +178,6 @@ export type ApiRouteManifestEntry = {
|
|
|
178
178
|
export type ModuleCli = {
|
|
179
179
|
command: string
|
|
180
180
|
run: (argv: string[]) => Promise<void> | void
|
|
181
|
-
// Usage text printed by the `mercato` dispatcher when the command is invoked with --help/-h.
|
|
182
|
-
help?: string
|
|
183
|
-
// Set when the command parses --help/-h itself; the dispatcher then forwards the flag
|
|
184
|
-
// instead of intercepting it, so the command can print its own richer usage text.
|
|
185
|
-
handlesHelp?: boolean
|
|
186
181
|
}
|
|
187
182
|
|
|
188
183
|
export type ModuleSubscriber = {
|
package/src/modules/search.ts
CHANGED
|
@@ -94,39 +94,6 @@ export type SearchOptions = {
|
|
|
94
94
|
offset?: number
|
|
95
95
|
/** How to combine results: 'or' merges all, 'and' requires match in all strategies */
|
|
96
96
|
combineMode?: 'or' | 'and'
|
|
97
|
-
/**
|
|
98
|
-
* Restricts results to records whose query-index row (`entity_indexes`) matches this predicate,
|
|
99
|
-
* evaluated inside the strategy's ranking query so a restrictive filter never starves the result
|
|
100
|
-
* set (additive, 2026-10-06). Only strategies with `supportsIndexDocFilter: true` receive a
|
|
101
|
-
* filtered search; any other strategy returns no results for it rather than unfiltered ones.
|
|
102
|
-
*/
|
|
103
|
-
indexDocFilter?: SearchIndexDocFilter
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* One condition over a record's query-index document (`entity_indexes.doc`), with the query
|
|
108
|
-
* engine's index-document semantics:
|
|
109
|
-
* - `exists`: the key is present and not JSON `null`;
|
|
110
|
-
* - `eq`: the key's text value equals `value` (booleans and numbers compare by their JSON text);
|
|
111
|
-
* - `overlap`: the key holds an array sharing at least one entry with `values`; empty `values`
|
|
112
|
-
* matches nothing;
|
|
113
|
-
* - `noverlap`: the key holds an array sharing no entry with `values`; a missing key never matches;
|
|
114
|
-
* - `recordIdNotIn`: the record id is none of `values`.
|
|
115
|
-
*/
|
|
116
|
-
export type SearchIndexDocCondition =
|
|
117
|
-
| { op: 'exists'; key: string }
|
|
118
|
-
| { op: 'eq'; key: string; value: string | number | boolean }
|
|
119
|
-
| { op: 'overlap'; key: string; values: string[] }
|
|
120
|
-
| { op: 'noverlap'; key: string; values: string[] }
|
|
121
|
-
| { op: 'recordIdNotIn'; values: string[] }
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* A predicate over a record's `entity_indexes` row in disjunctive normal form: the record matches
|
|
125
|
-
* when its index row is not deleted and every condition of at least one `anyOf` branch holds.
|
|
126
|
-
* An empty `anyOf` matches nothing; an empty branch matches every indexed record.
|
|
127
|
-
*/
|
|
128
|
-
export type SearchIndexDocFilter = {
|
|
129
|
-
anyOf: SearchIndexDocCondition[][]
|
|
130
97
|
}
|
|
131
98
|
|
|
132
99
|
// =============================================================================
|
|
@@ -177,12 +144,6 @@ export interface SearchStrategy {
|
|
|
177
144
|
/** Priority for result merging (higher = more prominent in results) */
|
|
178
145
|
readonly priority: number
|
|
179
146
|
|
|
180
|
-
/**
|
|
181
|
-
* Whether `search()` applies `SearchOptions.indexDocFilter` inside its ranking query. Absent or
|
|
182
|
-
* `false` means the strategy is skipped for filtered searches (fail closed).
|
|
183
|
-
*/
|
|
184
|
-
readonly supportsIndexDocFilter?: boolean
|
|
185
|
-
|
|
186
147
|
/** Check if strategy is available and configured */
|
|
187
148
|
isAvailable(): Promise<boolean>
|
|
188
149
|
|
|
@@ -141,41 +141,11 @@ function notifyInjectionRegistryChanged() {
|
|
|
141
141
|
}))
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
export
|
|
145
|
-
|
|
146
|
-
* `replace` (default) publishes `entries` as the whole registry.
|
|
147
|
-
*
|
|
148
|
-
* `merge` adds only the entries the registry does not already carry, keeping whatever a
|
|
149
|
-
* previous bootstrap published. A process may run more than one bootstrap partition (the
|
|
150
|
-
* API-only one and the full page one share a Node process in a mixed web host), and the
|
|
151
|
-
* partition that registers second must not shrink the registry the first one populated.
|
|
152
|
-
*/
|
|
153
|
-
mode?: 'replace' | 'merge'
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export function registerCoreInjectionWidgets(
|
|
157
|
-
entries: ModuleInjectionWidgetEntry[],
|
|
158
|
-
options: RegisterCoreInjectionWidgetsOptions = {},
|
|
159
|
-
) {
|
|
160
|
-
if (_coreInjectionWidgetEntries !== null && process.env.NODE_ENV === 'development' && options.mode !== 'merge') {
|
|
144
|
+
export function registerCoreInjectionWidgets(entries: ModuleInjectionWidgetEntry[]) {
|
|
145
|
+
if (_coreInjectionWidgetEntries !== null && process.env.NODE_ENV === 'development') {
|
|
161
146
|
logger.debug('Core injection widgets re-registered (this may occur during HMR)')
|
|
162
147
|
}
|
|
163
|
-
const
|
|
164
|
-
if (options.mode !== 'merge') {
|
|
165
|
-
_coreInjectionWidgetEntries = incoming
|
|
166
|
-
writeGlobalInjectionWidgets(incoming)
|
|
167
|
-
notifyInjectionRegistryChanged()
|
|
168
|
-
return
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const existing = readGlobalInjectionWidgets() ?? _coreInjectionWidgetEntries ?? []
|
|
172
|
-
const known = new Set(existing.map((entry) => entry.key))
|
|
173
|
-
const added = incoming.filter((entry) => !known.has(entry.key))
|
|
174
|
-
// Nothing to contribute: leave the registry exactly as it was. Publishing an empty array here
|
|
175
|
-
// would turn "never registered" into "registered with no widgets", which downgrades the loud
|
|
176
|
-
// bootstrap error into silently missing contributions.
|
|
177
|
-
if (!added.length) return
|
|
178
|
-
const finalEntries = [...existing, ...added]
|
|
148
|
+
const finalEntries = applyInjectionWidgetOverridesToEntries(entries)
|
|
179
149
|
_coreInjectionWidgetEntries = finalEntries
|
|
180
150
|
writeGlobalInjectionWidgets(finalEntries)
|
|
181
151
|
notifyInjectionRegistryChanged()
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../../src/lib/bootstrap/next-subpath-resolve-hook-worker.ts"],
|
|
4
|
-
"sourcesContent": ["import { resolveNextSubpathAsync } from './next-subpath-resolve-hook'\n\nexport const resolve = resolveNextSubpathAsync\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,+BAA+B;AAEjC,MAAM,UAAU;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|