@open-mercato/shared 0.6.7-develop.6795.1.8a3f27921c → 0.6.7-develop.6814.1.0627c7e9f1
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/dist/lib/di/container.js +33 -3
- package/dist/lib/di/container.js.map +2 -2
- package/dist/lib/email/send.js +56 -0
- package/dist/lib/email/send.js.map +2 -2
- package/dist/lib/query/types.js.map +1 -1
- package/dist/lib/search/config.js +24 -2
- package/dist/lib/search/config.js.map +2 -2
- package/dist/lib/search/tokenize.js +23 -11
- package/dist/lib/search/tokenize.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 +57 -0
- package/dist/lib/webhooks/body.js.map +7 -0
- package/dist/lib/webhooks/index.js +13 -1
- package/dist/lib/webhooks/index.js.map +2 -2
- package/dist/lib/webhooks/verify.js +12 -7
- package/dist/lib/webhooks/verify.js.map +2 -2
- package/dist/modules/payment_gateways/types.js +8 -1
- package/dist/modules/payment_gateways/types.js.map +2 -2
- package/dist/modules/widgets/extension-points.js.map +1 -1
- package/package.json +2 -2
- package/src/lib/di/__tests__/container-app-di-absent.test.ts +139 -0
- package/src/lib/di/__tests__/container-app-di.test.ts +173 -0
- package/src/lib/di/container.ts +45 -4
- package/src/lib/email/__tests__/send.test.ts +37 -1
- package/src/lib/email/send.ts +79 -0
- package/src/lib/query/types.ts +4 -5
- package/src/lib/search/__tests__/config.test.ts +59 -0
- package/src/lib/search/__tests__/tokenize.test.ts +49 -0
- package/src/lib/search/config.ts +28 -0
- package/src/lib/search/tokenize.ts +33 -11
- package/src/lib/webhooks/__tests__/body.test.ts +95 -0
- package/src/lib/webhooks/__tests__/verify.test.ts +20 -1
- package/src/lib/webhooks/body.ts +70 -0
- package/src/lib/webhooks/index.ts +7 -1
- package/src/lib/webhooks/verify.ts +17 -9
- package/src/modules/payment_gateways/__tests__/types.test.ts +37 -0
- package/src/modules/payment_gateways/types.ts +15 -0
- package/src/modules/widgets/extension-points.ts +104 -1
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export { signWebhookPayload, buildWebhookHeaders, generateMessageId } from './sign'
|
|
2
|
-
export { verifyWebhookSignature } from './verify'
|
|
2
|
+
export { WEBHOOK_SIGNATURE_TOLERANCE_SECONDS, isWebhookTimestampWithinTolerance, verifyWebhookSignature } from './verify'
|
|
3
3
|
export { generateWebhookSecret, parseWebhookSecret, isValidWebhookSecret } from './secrets'
|
|
4
|
+
export {
|
|
5
|
+
DEFAULT_WEBHOOK_BODY_LIMIT_BYTES,
|
|
6
|
+
readBoundedRequestBody,
|
|
7
|
+
resolveWebhookBodyLimitBytes,
|
|
8
|
+
WebhookBodyTooLargeError,
|
|
9
|
+
} from './body'
|
|
4
10
|
export type { StandardWebhookHeaders, WebhookSigningKey, WebhookVerificationResult, StandardWebhookPayload } from './types'
|
|
5
11
|
export type {
|
|
6
12
|
InboundWebhookRequest,
|
|
@@ -2,7 +2,21 @@ import { timingSafeEqual, createHmac } from 'node:crypto'
|
|
|
2
2
|
import { parseWebhookSecret } from './secrets'
|
|
3
3
|
import type { WebhookVerificationResult } from './types'
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
export const WEBHOOK_SIGNATURE_TOLERANCE_SECONDS = 5 * 60 // 5 minutes
|
|
6
|
+
|
|
7
|
+
export function isWebhookTimestampWithinTolerance(
|
|
8
|
+
timestamp: string,
|
|
9
|
+
toleranceSeconds: number = WEBHOOK_SIGNATURE_TOLERANCE_SECONDS,
|
|
10
|
+
nowSeconds: number = Math.floor(Date.now() / 1000),
|
|
11
|
+
): boolean {
|
|
12
|
+
const normalizedTimestamp = timestamp.trim()
|
|
13
|
+
if (!/^\d+$/.test(normalizedTimestamp)) {
|
|
14
|
+
return false
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const timestampNum = Number.parseInt(normalizedTimestamp, 10)
|
|
18
|
+
return Math.abs(nowSeconds - timestampNum) <= toleranceSeconds
|
|
19
|
+
}
|
|
6
20
|
|
|
7
21
|
/**
|
|
8
22
|
* Verify a Standard Webhooks signature against one or more secrets.
|
|
@@ -14,15 +28,9 @@ export function verifyWebhookSignature(
|
|
|
14
28
|
body: string,
|
|
15
29
|
signatureHeader: string,
|
|
16
30
|
secrets: string[],
|
|
17
|
-
toleranceSeconds: number =
|
|
31
|
+
toleranceSeconds: number = WEBHOOK_SIGNATURE_TOLERANCE_SECONDS,
|
|
18
32
|
): WebhookVerificationResult {
|
|
19
|
-
|
|
20
|
-
if (isNaN(timestampNum)) {
|
|
21
|
-
return { valid: false }
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const now = Math.floor(Date.now() / 1000)
|
|
25
|
-
if (Math.abs(now - timestampNum) > toleranceSeconds) {
|
|
33
|
+
if (!isWebhookTimestampWithinTolerance(timestamp, toleranceSeconds)) {
|
|
26
34
|
return { valid: false }
|
|
27
35
|
}
|
|
28
36
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clearWebhookHandlers,
|
|
3
|
+
getWebhookHandler,
|
|
4
|
+
registerWebhookHandler,
|
|
5
|
+
type VerifyWebhookInput,
|
|
6
|
+
type WebhookEvent,
|
|
7
|
+
} from '../types'
|
|
8
|
+
|
|
9
|
+
const handler = async (_input: VerifyWebhookInput): Promise<WebhookEvent> => ({
|
|
10
|
+
eventType: 'payment.updated',
|
|
11
|
+
eventId: 'evt-1',
|
|
12
|
+
data: {},
|
|
13
|
+
idempotencyKey: 'evt-1',
|
|
14
|
+
timestamp: new Date(0),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('payment gateway webhook registration', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
clearWebhookHandlers()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('preserves an optional valid body limit on the registration', () => {
|
|
23
|
+
registerWebhookHandler('limited', handler, { maxBodyBytes: 64 * 1024 })
|
|
24
|
+
|
|
25
|
+
expect(getWebhookHandler('limited')?.maxBodyBytes).toBe(64 * 1024)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it.each([0, -1, 1.5, Number.MAX_SAFE_INTEGER])(
|
|
29
|
+
'rejects an invalid body limit: %s',
|
|
30
|
+
(maxBodyBytes) => {
|
|
31
|
+
expect(() => registerWebhookHandler('invalid', handler, { maxBodyBytes })).toThrow(
|
|
32
|
+
'[internal] Payment gateway webhook maxBodyBytes must be a positive safe integer',
|
|
33
|
+
)
|
|
34
|
+
expect(getWebhookHandler('invalid')).toBeUndefined()
|
|
35
|
+
},
|
|
36
|
+
)
|
|
37
|
+
})
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { DEFAULT_WEBHOOK_BODY_LIMIT_BYTES } from '../../lib/webhooks/body'
|
|
2
|
+
|
|
1
3
|
// ── Unified Payment Status ──────────────────────────────────────────────────
|
|
2
4
|
|
|
3
5
|
export type UnifiedPaymentStatus =
|
|
@@ -251,6 +253,7 @@ export interface WebhookHandlerRegistration {
|
|
|
251
253
|
handler: (input: VerifyWebhookInput) => Promise<WebhookEvent>
|
|
252
254
|
queue?: string
|
|
253
255
|
readSessionIdHint?: (payload: Record<string, unknown> | null) => string | null
|
|
256
|
+
maxBodyBytes?: number
|
|
254
257
|
}
|
|
255
258
|
|
|
256
259
|
// ── Adapter Registry Options ────────────────────────────────────────────────
|
|
@@ -345,13 +348,25 @@ export function registerWebhookHandler(
|
|
|
345
348
|
options?: {
|
|
346
349
|
queue?: string
|
|
347
350
|
readSessionIdHint?: (payload: Record<string, unknown> | null) => string | null
|
|
351
|
+
maxBodyBytes?: number
|
|
348
352
|
},
|
|
349
353
|
): () => void {
|
|
354
|
+
if (
|
|
355
|
+
options?.maxBodyBytes !== undefined
|
|
356
|
+
&& (!Number.isSafeInteger(options.maxBodyBytes)
|
|
357
|
+
|| options.maxBodyBytes <= 0
|
|
358
|
+
|| options.maxBodyBytes > DEFAULT_WEBHOOK_BODY_LIMIT_BYTES)
|
|
359
|
+
) {
|
|
360
|
+
throw new Error(
|
|
361
|
+
`[internal] Payment gateway webhook maxBodyBytes must be a positive safe integer no greater than ${DEFAULT_WEBHOOK_BODY_LIMIT_BYTES}`,
|
|
362
|
+
)
|
|
363
|
+
}
|
|
350
364
|
const webhookHandlerRegistry = getWebhookHandlerRegistry()
|
|
351
365
|
webhookHandlerRegistry.set(providerKey, {
|
|
352
366
|
handler,
|
|
353
367
|
queue: options?.queue,
|
|
354
368
|
readSessionIdHint: options?.readSessionIdHint,
|
|
369
|
+
maxBodyBytes: options?.maxBodyBytes,
|
|
355
370
|
})
|
|
356
371
|
return () => {
|
|
357
372
|
webhookHandlerRegistry.delete(providerKey)
|
|
@@ -232,11 +232,108 @@ export function resolveExtensionPointPattern(
|
|
|
232
232
|
return pattern.replace(/\{([^}]+)\}/g, (token, parameterName: string) => parameters[parameterName] ?? token)
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Portable, structural copy of the CLI provenance source reference
|
|
237
|
+
* (`ModuleFactSourceRef` in `packages/cli/.../module-facts.ts`). Redefined here
|
|
238
|
+
* — rather than imported — so `@open-mercato/shared` keeps zero dependency on
|
|
239
|
+
* `@open-mercato/cli`. Field names are kept byte-identical so the two are
|
|
240
|
+
* assignable across the package boundary.
|
|
241
|
+
*/
|
|
242
|
+
export type ModuleFactSourceRef = {
|
|
243
|
+
sourcePath: string
|
|
244
|
+
exportName?: string
|
|
245
|
+
line?: number
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Portable, structural copy of the CLI provenance fact reference. */
|
|
249
|
+
export type ModuleFactRef = {
|
|
250
|
+
factSection: string
|
|
251
|
+
factKey: string
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export type ModuleExtensionContributionKind = ModuleExtensionContributionFact['kind']
|
|
255
|
+
|
|
256
|
+
export type ModuleExtensionActivationKind =
|
|
257
|
+
| 'crud-response-enricher'
|
|
258
|
+
| 'query-enricher'
|
|
259
|
+
| 'mutation-guard'
|
|
260
|
+
| 'api-interceptor-bridge'
|
|
261
|
+
| 'command-interceptor-bridge'
|
|
262
|
+
| 'widget-injection-consumer'
|
|
263
|
+
| 'component-extension-consumer'
|
|
264
|
+
| 'dashboard-host-consumer'
|
|
265
|
+
|
|
266
|
+
export type ModuleExtensionTargetRef = {
|
|
267
|
+
kind:
|
|
268
|
+
| 'module'
|
|
269
|
+
| 'entity'
|
|
270
|
+
| 'api-route'
|
|
271
|
+
| 'command'
|
|
272
|
+
| 'widget-spot'
|
|
273
|
+
| 'component'
|
|
274
|
+
| 'event'
|
|
275
|
+
| 'notification'
|
|
276
|
+
| 'wildcard'
|
|
277
|
+
id: string
|
|
278
|
+
moduleId?: string
|
|
279
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export type ModuleExtensionActivation = {
|
|
283
|
+
id: string
|
|
284
|
+
kind: ModuleExtensionActivationKind
|
|
285
|
+
host: ModuleExtensionTargetRef
|
|
286
|
+
contributionKinds: ModuleExtensionContributionKind[]
|
|
287
|
+
phases?: string[]
|
|
288
|
+
/**
|
|
289
|
+
* Mutation operations the call site guards, when statically declared. A
|
|
290
|
+
* contribution declaring only other operations is not bound by this activation.
|
|
291
|
+
*/
|
|
292
|
+
operations?: string[]
|
|
293
|
+
source: ModuleFactSourceRef
|
|
294
|
+
bridge?: ModuleFactRef
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export type ModuleExtensionResolution =
|
|
298
|
+
| 'bound'
|
|
299
|
+
| 'capability-only'
|
|
300
|
+
| 'optional-target-missing'
|
|
301
|
+
| 'wildcard'
|
|
302
|
+
| 'unresolved'
|
|
303
|
+
|
|
304
|
+
export type ModuleIncomingExtensionRef = {
|
|
305
|
+
contributionId: string
|
|
306
|
+
contributionKind: ModuleExtensionContributionKind
|
|
307
|
+
contributorModuleId: string
|
|
308
|
+
target: ModuleExtensionTargetRef
|
|
309
|
+
activationId?: string
|
|
310
|
+
resolution: ModuleExtensionResolution
|
|
311
|
+
source: ModuleFactSourceRef
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export type ModuleContributionResolution = {
|
|
315
|
+
contributionId: string
|
|
316
|
+
target: ModuleExtensionTargetRef
|
|
317
|
+
resolution: ModuleExtensionResolution
|
|
318
|
+
activationIds: string[]
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Additive, optional bidirectional-topology fields layered on top of the
|
|
323
|
+
* existing extension-surface facts. Every field is optional so legacy consumers
|
|
324
|
+
* and pre-existing generated output remain byte-compatible.
|
|
325
|
+
*/
|
|
326
|
+
export type ModuleExtensionSurfaceFactsAdditions = {
|
|
327
|
+
activations?: ModuleExtensionActivation[]
|
|
328
|
+
incoming?: ModuleIncomingExtensionRef[]
|
|
329
|
+
contributionResolutions?: ModuleContributionResolution[]
|
|
330
|
+
}
|
|
331
|
+
|
|
235
332
|
export type ModuleExtensionSurfaceFacts = {
|
|
236
333
|
hosts: ModuleExtensionHostFact[]
|
|
237
334
|
contributions: ModuleExtensionContributionFact[]
|
|
238
335
|
unresolved: ModuleExtensionUnresolvedFact[]
|
|
239
|
-
}
|
|
336
|
+
} & ModuleExtensionSurfaceFactsAdditions
|
|
240
337
|
|
|
241
338
|
export type ModuleExtensionHostFact = {
|
|
242
339
|
key: string
|
|
@@ -390,6 +487,12 @@ export type ModuleExtensionContributionFact = ModuleExtensionContributionBase &
|
|
|
390
487
|
audienceScopeContract: string
|
|
391
488
|
maxPayloadBytes?: number
|
|
392
489
|
dedupWindowMs?: number
|
|
490
|
+
/**
|
|
491
|
+
* Statically declared handler id. Runtime keys `notifications.handlers`
|
|
492
|
+
* overrides by it, so it is absent when the module declares no id (such a
|
|
493
|
+
* handler cannot be overridden).
|
|
494
|
+
*/
|
|
495
|
+
overrideKey?: string
|
|
393
496
|
}
|
|
394
497
|
}
|
|
395
498
|
| {
|