@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.
Files changed (40) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/lib/di/container.js +33 -3
  3. package/dist/lib/di/container.js.map +2 -2
  4. package/dist/lib/email/send.js +56 -0
  5. package/dist/lib/email/send.js.map +2 -2
  6. package/dist/lib/query/types.js.map +1 -1
  7. package/dist/lib/search/config.js +24 -2
  8. package/dist/lib/search/config.js.map +2 -2
  9. package/dist/lib/search/tokenize.js +23 -11
  10. package/dist/lib/search/tokenize.js.map +2 -2
  11. package/dist/lib/version.js +1 -1
  12. package/dist/lib/version.js.map +1 -1
  13. package/dist/lib/webhooks/body.js +57 -0
  14. package/dist/lib/webhooks/body.js.map +7 -0
  15. package/dist/lib/webhooks/index.js +13 -1
  16. package/dist/lib/webhooks/index.js.map +2 -2
  17. package/dist/lib/webhooks/verify.js +12 -7
  18. package/dist/lib/webhooks/verify.js.map +2 -2
  19. package/dist/modules/payment_gateways/types.js +8 -1
  20. package/dist/modules/payment_gateways/types.js.map +2 -2
  21. package/dist/modules/widgets/extension-points.js.map +1 -1
  22. package/package.json +2 -2
  23. package/src/lib/di/__tests__/container-app-di-absent.test.ts +139 -0
  24. package/src/lib/di/__tests__/container-app-di.test.ts +173 -0
  25. package/src/lib/di/container.ts +45 -4
  26. package/src/lib/email/__tests__/send.test.ts +37 -1
  27. package/src/lib/email/send.ts +79 -0
  28. package/src/lib/query/types.ts +4 -5
  29. package/src/lib/search/__tests__/config.test.ts +59 -0
  30. package/src/lib/search/__tests__/tokenize.test.ts +49 -0
  31. package/src/lib/search/config.ts +28 -0
  32. package/src/lib/search/tokenize.ts +33 -11
  33. package/src/lib/webhooks/__tests__/body.test.ts +95 -0
  34. package/src/lib/webhooks/__tests__/verify.test.ts +20 -1
  35. package/src/lib/webhooks/body.ts +70 -0
  36. package/src/lib/webhooks/index.ts +7 -1
  37. package/src/lib/webhooks/verify.ts +17 -9
  38. package/src/modules/payment_gateways/__tests__/types.test.ts +37 -0
  39. package/src/modules/payment_gateways/types.ts +15 -0
  40. 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 TOLERANCE_SECONDS = 5 * 60 // 5 minutes
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 = TOLERANCE_SECONDS,
31
+ toleranceSeconds: number = WEBHOOK_SIGNATURE_TOLERANCE_SECONDS,
18
32
  ): WebhookVerificationResult {
19
- const timestampNum = parseInt(timestamp, 10)
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
  | {