@open-mercato/shared 0.6.7 → 0.6.8-develop.6874.1.982d6097d8
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 +57 -6
- package/build.mjs +2 -4
- package/dist/lib/ai/safety-identifier.js +32 -0
- package/dist/lib/ai/safety-identifier.js.map +7 -0
- package/dist/lib/auth/organizationScope.js +34 -0
- package/dist/lib/auth/organizationScope.js.map +7 -0
- package/dist/lib/auth/server.js +15 -2
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/bootstrap/clientOnlyModules.js +55 -0
- package/dist/lib/bootstrap/clientOnlyModules.js.map +7 -0
- package/dist/lib/bootstrap/dynamicLoader.js +117 -43
- package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
- package/dist/lib/commands/command-bus.js +5 -1
- package/dist/lib/commands/command-bus.js.map +2 -2
- package/dist/lib/commands/command-interceptor-runner.js +2 -2
- package/dist/lib/commands/command-interceptor-runner.js.map +2 -2
- package/dist/lib/crud/enricher-runner.js +2 -11
- package/dist/lib/crud/enricher-runner.js.map +2 -2
- package/dist/lib/crud/factory.js +72 -9
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/crud/interceptor-runner.js +2 -2
- package/dist/lib/crud/interceptor-runner.js.map +2 -2
- package/dist/lib/crud/mutation-guard-registry.js +2 -2
- package/dist/lib/crud/mutation-guard-registry.js.map +2 -2
- package/dist/lib/crud/types.js +4 -0
- package/dist/lib/crud/types.js.map +3 -3
- package/dist/lib/data/consistency.js +19 -0
- package/dist/lib/data/consistency.js.map +7 -0
- package/dist/lib/data/engine.js +37 -11
- package/dist/lib/data/engine.js.map +2 -2
- package/dist/lib/db/entityMetadata.js +26 -0
- package/dist/lib/db/entityMetadata.js.map +7 -0
- package/dist/lib/db/pg-errors.js +42 -0
- package/dist/lib/db/pg-errors.js.map +2 -2
- package/dist/lib/di/container.js +48 -6
- 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/encryption/subscriber.js +2 -10
- package/dist/lib/encryption/subscriber.js.map +2 -2
- package/dist/lib/encryption/tenantDataEncryptionService.js +52 -17
- package/dist/lib/encryption/tenantDataEncryptionService.js.map +2 -2
- package/dist/lib/i18n/config.js +1 -1
- package/dist/lib/i18n/config.js.map +2 -2
- package/dist/lib/i18n/server.js +4 -4
- package/dist/lib/i18n/server.js.map +3 -3
- package/dist/lib/logger/extension.js +29 -0
- package/dist/lib/logger/extension.js.map +7 -0
- package/dist/lib/logger/index.js +47 -3
- package/dist/lib/logger/index.js.map +3 -3
- package/dist/lib/modules/registry.js +5 -1
- package/dist/lib/modules/registry.js.map +2 -2
- package/dist/lib/modules/surfaceFingerprint.js +47 -0
- package/dist/lib/modules/surfaceFingerprint.js.map +7 -0
- package/dist/lib/query/ciphertext-search-warning.js +45 -0
- package/dist/lib/query/ciphertext-search-warning.js.map +7 -0
- package/dist/lib/query/engine.js +54 -40
- package/dist/lib/query/engine.js.map +2 -2
- package/dist/lib/query/types.js.map +1 -1
- package/dist/lib/redis/connection.js +7 -2
- package/dist/lib/redis/connection.js.map +2 -2
- package/dist/lib/search/auto-indexing.js +14 -0
- package/dist/lib/search/auto-indexing.js.map +7 -0
- package/dist/lib/search/availability.js +107 -0
- package/dist/lib/search/availability.js.map +7 -0
- package/dist/lib/search/config.js +61 -2
- package/dist/lib/search/config.js.map +2 -2
- package/dist/lib/search/tokenLookup.js +46 -0
- package/dist/lib/search/tokenLookup.js.map +7 -0
- package/dist/lib/search/tokenize.js +23 -11
- package/dist/lib/search/tokenize.js.map +2 -2
- package/dist/lib/telemetry/runtime.js +35 -0
- package/dist/lib/telemetry/runtime.js.map +7 -0
- 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/inbound-types.js +1 -0
- package/dist/lib/webhooks/inbound-types.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/analytics.js.map +2 -2
- package/dist/modules/overrides.js +50 -1
- package/dist/modules/overrides.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/registry.js +1 -0
- package/dist/modules/registry.js.map +2 -2
- package/dist/modules/widgets/extension-points.js +96 -0
- package/dist/modules/widgets/extension-points.js.map +7 -0
- package/dist/security/enabledModulesRegistry.js +56 -11
- package/dist/security/enabledModulesRegistry.js.map +2 -2
- package/dist/security/featurePolicy.js +62 -0
- package/dist/security/featurePolicy.js.map +7 -0
- package/jest.config.cjs +2 -1
- package/package.json +19 -9
- package/scripts/versionSource.cjs +65 -0
- package/src/lib/__tests__/versionSource.test.ts +46 -0
- package/src/lib/ai/__tests__/llm-provider-contract.test.ts +59 -0
- package/src/lib/ai/__tests__/safety-identifier.test.ts +71 -0
- package/src/lib/ai/llm-provider.ts +33 -0
- package/src/lib/ai/safety-identifier.ts +73 -0
- package/src/lib/auth/__tests__/organizationScope.test.ts +107 -0
- package/src/lib/auth/__tests__/server.test.ts +23 -0
- package/src/lib/auth/organizationScope.ts +65 -0
- package/src/lib/auth/server.ts +23 -2
- package/src/lib/bootstrap/__tests__/clientOnlyModules.test.ts +189 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.aliasResolver.test.ts +98 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +211 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +63 -8
- package/src/lib/bootstrap/__tests__/dynamicLoader.generatedCacheRecovery.test.ts +256 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +21 -1
- package/src/lib/bootstrap/clientOnlyModules.ts +85 -0
- package/src/lib/bootstrap/dynamicLoader.ts +213 -65
- package/src/lib/commands/__tests__/command-interceptor-runner.test.ts +28 -0
- package/src/lib/commands/command-bus.ts +5 -1
- package/src/lib/commands/command-interceptor-runner.ts +2 -2
- package/src/lib/crud/__tests__/crud-factory.cache-user-scope.test.ts +171 -0
- package/src/lib/crud/__tests__/crud-factory.test.ts +351 -0
- package/src/lib/crud/__tests__/mutation-guard-registry.test.ts +26 -0
- package/src/lib/crud/enricher-runner.ts +2 -11
- package/src/lib/crud/factory.ts +82 -6
- package/src/lib/crud/interceptor-runner.ts +2 -2
- package/src/lib/crud/mutation-guard-registry.ts +2 -2
- package/src/lib/crud/types.ts +3 -0
- package/src/lib/data/__tests__/consistency.test.ts +38 -0
- package/src/lib/data/__tests__/engine.bulk-suppress.test.ts +18 -3
- package/src/lib/data/consistency.ts +17 -0
- package/src/lib/data/engine.ts +50 -16
- package/src/lib/db/__tests__/entityMetadata.test.ts +38 -0
- package/src/lib/db/__tests__/pg-errors.test.ts +48 -0
- package/src/lib/db/entityMetadata.ts +43 -0
- package/src/lib/db/pg-errors.ts +58 -0
- package/src/lib/di/__tests__/container-app-di-absent.test.ts +155 -0
- package/src/lib/di/__tests__/container-app-di.test.ts +173 -0
- package/src/lib/di/__tests__/registrar-error-log.test.ts +154 -0
- package/src/lib/di/container.ts +72 -8
- package/src/lib/email/__tests__/send.test.ts +37 -1
- package/src/lib/email/send.ts +79 -0
- package/src/lib/encryption/__tests__/subscriber.test.ts +54 -2
- package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +58 -0
- package/src/lib/encryption/subscriber.ts +2 -14
- package/src/lib/encryption/tenantDataEncryptionService.ts +64 -18
- package/src/lib/i18n/__tests__/config.test.ts +8 -0
- package/src/lib/i18n/__tests__/server-dictionary-cache.test.ts +30 -0
- package/src/lib/i18n/__tests__/server-unbootstrapped.test.ts +29 -0
- package/src/lib/i18n/config.ts +2 -2
- package/src/lib/i18n/server.ts +6 -2
- package/src/lib/logger/__tests__/logger.test.ts +77 -0
- package/src/lib/logger/extension.ts +59 -0
- package/src/lib/logger/index.ts +59 -1
- package/src/lib/modules/__tests__/surfaceFingerprint.test.ts +122 -0
- package/src/lib/modules/registry.ts +10 -0
- package/src/lib/modules/surfaceFingerprint.ts +87 -0
- package/src/lib/query/__tests__/ciphertext-search-warning.test.ts +178 -0
- package/src/lib/query/__tests__/engine.test.ts +65 -13
- package/src/lib/query/ciphertext-search-warning.ts +95 -0
- package/src/lib/query/engine.ts +75 -58
- package/src/lib/query/types.ts +4 -5
- package/src/lib/redis/__tests__/connection.test.ts +16 -0
- package/src/lib/redis/connection.ts +10 -7
- package/src/lib/search/__tests__/availability.test.ts +211 -0
- package/src/lib/search/__tests__/config.test.ts +177 -0
- package/src/lib/search/__tests__/tokenLookup.test.ts +206 -0
- package/src/lib/search/__tests__/tokenize.test.ts +49 -0
- package/src/lib/search/auto-indexing.ts +22 -0
- package/src/lib/search/availability.ts +232 -0
- package/src/lib/search/config.ts +106 -8
- package/src/lib/search/tokenLookup.ts +133 -0
- package/src/lib/search/tokenize.ts +33 -11
- package/src/lib/telemetry/runtime.ts +72 -0
- 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/inbound-types.ts +125 -0
- package/src/lib/webhooks/index.ts +19 -1
- package/src/lib/webhooks/verify.ts +17 -9
- package/src/modules/__tests__/nav-group-order-override.test.ts +183 -0
- package/src/modules/__tests__/registry.test.ts +10 -2
- package/src/modules/analytics.ts +7 -0
- package/src/modules/customer-auth.ts +1 -0
- package/src/modules/encryption.ts +3 -0
- package/src/modules/navigation/backendChrome.ts +15 -0
- package/src/modules/overrides.ts +103 -0
- package/src/modules/payment_gateways/__tests__/types.test.ts +37 -0
- package/src/modules/payment_gateways/types.ts +41 -0
- package/src/modules/registry.ts +3 -0
- package/src/modules/widgets/__tests__/extension-points.test.ts +101 -0
- package/src/modules/widgets/extension-points.ts +524 -0
- package/src/security/__tests__/featurePolicy.test.ts +166 -0
- package/src/security/enabledModulesRegistry.ts +64 -12
- package/src/security/featurePolicy.ts +89 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/** @jest-environment node */
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_WEBHOOK_BODY_LIMIT_BYTES,
|
|
4
|
+
readBoundedRequestBody,
|
|
5
|
+
resolveWebhookBodyLimitBytes,
|
|
6
|
+
WebhookBodyTooLargeError,
|
|
7
|
+
} from '../body'
|
|
8
|
+
|
|
9
|
+
function makeStreamingRequest(chunks: Uint8Array[], headers?: HeadersInit) {
|
|
10
|
+
let index = 0
|
|
11
|
+
const cancel = jest.fn()
|
|
12
|
+
const body = new ReadableStream<Uint8Array>({
|
|
13
|
+
pull(controller) {
|
|
14
|
+
const chunk = chunks[index]
|
|
15
|
+
index += 1
|
|
16
|
+
if (chunk) controller.enqueue(chunk)
|
|
17
|
+
else controller.close()
|
|
18
|
+
},
|
|
19
|
+
cancel,
|
|
20
|
+
})
|
|
21
|
+
const request = new Request('http://localhost/webhook', {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers,
|
|
24
|
+
body,
|
|
25
|
+
duplex: 'half',
|
|
26
|
+
} as RequestInit & { duplex: 'half' })
|
|
27
|
+
return { request, cancel }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('readBoundedRequestBody', () => {
|
|
31
|
+
const encoder = new TextEncoder()
|
|
32
|
+
|
|
33
|
+
it('rejects an oversized declared length before reading the body', async () => {
|
|
34
|
+
const request = new Request('http://localhost/webhook', {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: { 'content-length': '6' },
|
|
37
|
+
body: 'ok',
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
await expect(readBoundedRequestBody(request, { maxBytes: 5 })).rejects.toEqual(
|
|
41
|
+
expect.objectContaining<WebhookBodyTooLargeError>({ limitBytes: 5 }),
|
|
42
|
+
)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('stops a chunked body when its actual byte count crosses the limit', async () => {
|
|
46
|
+
const { request, cancel } = makeStreamingRequest([
|
|
47
|
+
encoder.encode('abc'),
|
|
48
|
+
encoder.encode('def'),
|
|
49
|
+
encoder.encode('never-read'),
|
|
50
|
+
])
|
|
51
|
+
|
|
52
|
+
await expect(readBoundedRequestBody(request, { maxBytes: 5 })).rejects.toBeInstanceOf(
|
|
53
|
+
WebhookBodyTooLargeError,
|
|
54
|
+
)
|
|
55
|
+
expect(cancel).toHaveBeenCalled()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it.each([
|
|
59
|
+
['invalid', 'not-a-number'],
|
|
60
|
+
['lying', '3'],
|
|
61
|
+
])('does not let a %s Content-Length bypass the streamed cap', async (_case, contentLength) => {
|
|
62
|
+
const { request } = makeStreamingRequest(
|
|
63
|
+
[encoder.encode('abc'), encoder.encode('def')],
|
|
64
|
+
{ 'content-length': contentLength },
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
await expect(readBoundedRequestBody(request, { maxBytes: 5 })).rejects.toBeInstanceOf(
|
|
68
|
+
WebhookBodyTooLargeError,
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('preserves the exact decoded body at the byte boundary', async () => {
|
|
73
|
+
const bytes = encoder.encode('a€')
|
|
74
|
+
const { request } = makeStreamingRequest([bytes.slice(0, 2), bytes.slice(2)])
|
|
75
|
+
|
|
76
|
+
await expect(readBoundedRequestBody(request, { maxBytes: bytes.byteLength })).resolves.toBe('a€')
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('resolveWebhookBodyLimitBytes', () => {
|
|
81
|
+
it('uses the documented default for missing or invalid configuration', () => {
|
|
82
|
+
expect(resolveWebhookBodyLimitBytes(undefined)).toBe(DEFAULT_WEBHOOK_BODY_LIMIT_BYTES)
|
|
83
|
+
expect(resolveWebhookBodyLimitBytes('invalid')).toBe(DEFAULT_WEBHOOK_BODY_LIMIT_BYTES)
|
|
84
|
+
expect(resolveWebhookBodyLimitBytes('0')).toBe(DEFAULT_WEBHOOK_BODY_LIMIT_BYTES)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('accepts a positive safe integer override', () => {
|
|
88
|
+
expect(resolveWebhookBodyLimitBytes('2097152')).toBe(2 * 1024 * 1024)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('supports a source-specific fallback without weakening validation', () => {
|
|
92
|
+
expect(resolveWebhookBodyLimitBytes(undefined, 2 * 1024 * 1024)).toBe(2 * 1024 * 1024)
|
|
93
|
+
expect(resolveWebhookBodyLimitBytes('invalid', 2 * 1024 * 1024)).toBe(2 * 1024 * 1024)
|
|
94
|
+
})
|
|
95
|
+
})
|
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
import { signWebhookPayload } from '../sign'
|
|
2
|
-
import { verifyWebhookSignature } from '../verify'
|
|
2
|
+
import { isWebhookTimestampWithinTolerance, verifyWebhookSignature } from '../verify'
|
|
3
3
|
import { generateWebhookSecret } from '../secrets'
|
|
4
4
|
|
|
5
|
+
describe('isWebhookTimestampWithinTolerance', () => {
|
|
6
|
+
it('accepts timestamps inside the configured tolerance', () => {
|
|
7
|
+
expect(isWebhookTimestampWithinTolerance('1700000000', 300, 1700000299)).toBe(true)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('rejects stale timestamps outside the configured tolerance', () => {
|
|
11
|
+
expect(isWebhookTimestampWithinTolerance('1700000000', 300, 1700000301)).toBe(false)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('rejects timestamps too far in the future', () => {
|
|
15
|
+
expect(isWebhookTimestampWithinTolerance('1700000601', 300, 1700000000)).toBe(false)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('rejects malformed timestamps', () => {
|
|
19
|
+
expect(isWebhookTimestampWithinTolerance('not-a-timestamp', 300, 1700000000)).toBe(false)
|
|
20
|
+
expect(isWebhookTimestampWithinTolerance('1700000000abc', 300, 1700000000)).toBe(false)
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
5
24
|
describe('verifyWebhookSignature', () => {
|
|
6
25
|
const msgId = 'msg_test123'
|
|
7
26
|
const body = '{"type":"test.event","data":{}}'
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { parseNumberWithDefault } from '../number'
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_WEBHOOK_BODY_LIMIT_BYTES = 1024 * 1024
|
|
4
|
+
|
|
5
|
+
export class WebhookBodyTooLargeError extends Error {
|
|
6
|
+
readonly limitBytes: number
|
|
7
|
+
|
|
8
|
+
constructor(limitBytes: number) {
|
|
9
|
+
super(`Webhook body exceeds the ${limitBytes}-byte limit`)
|
|
10
|
+
this.name = 'WebhookBodyTooLargeError'
|
|
11
|
+
this.limitBytes = limitBytes
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function resolveWebhookBodyLimitBytes(
|
|
16
|
+
raw = typeof process === 'undefined' ? undefined : process.env.OM_WEBHOOK_MAX_BODY_BYTES,
|
|
17
|
+
fallbackBytes = DEFAULT_WEBHOOK_BODY_LIMIT_BYTES,
|
|
18
|
+
): number {
|
|
19
|
+
const fallback = Number.isSafeInteger(fallbackBytes) && fallbackBytes > 0
|
|
20
|
+
? fallbackBytes
|
|
21
|
+
: DEFAULT_WEBHOOK_BODY_LIMIT_BYTES
|
|
22
|
+
const parsed = parseNumberWithDefault(raw, fallback, { min: 1 })
|
|
23
|
+
return Number.isSafeInteger(parsed) ? parsed : fallback
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function readBoundedRequestBody(
|
|
27
|
+
request: Request,
|
|
28
|
+
options?: { maxBytes?: number },
|
|
29
|
+
): Promise<string> {
|
|
30
|
+
const configuredLimit = options?.maxBytes ?? resolveWebhookBodyLimitBytes()
|
|
31
|
+
const maxBytes = Number.isSafeInteger(configuredLimit) && configuredLimit > 0
|
|
32
|
+
? configuredLimit
|
|
33
|
+
: DEFAULT_WEBHOOK_BODY_LIMIT_BYTES
|
|
34
|
+
const declaredLength = request.headers.get('content-length')?.trim()
|
|
35
|
+
if (declaredLength && /^\d+$/.test(declaredLength)) {
|
|
36
|
+
const declaredBytes = Number(declaredLength)
|
|
37
|
+
if (!Number.isSafeInteger(declaredBytes) || declaredBytes > maxBytes) {
|
|
38
|
+
throw new WebhookBodyTooLargeError(maxBytes)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!request.body) return ''
|
|
43
|
+
|
|
44
|
+
const reader = request.body.getReader()
|
|
45
|
+
const chunks: Uint8Array[] = []
|
|
46
|
+
let totalBytes = 0
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
while (true) {
|
|
50
|
+
const { done, value } = await reader.read()
|
|
51
|
+
if (done) break
|
|
52
|
+
totalBytes += value.byteLength
|
|
53
|
+
if (totalBytes > maxBytes) {
|
|
54
|
+
await reader.cancel().catch(() => undefined)
|
|
55
|
+
throw new WebhookBodyTooLargeError(maxBytes)
|
|
56
|
+
}
|
|
57
|
+
chunks.push(value)
|
|
58
|
+
}
|
|
59
|
+
} finally {
|
|
60
|
+
reader.releaseLock()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const body = new Uint8Array(totalBytes)
|
|
64
|
+
let offset = 0
|
|
65
|
+
for (const chunk of chunks) {
|
|
66
|
+
body.set(chunk, offset)
|
|
67
|
+
offset += chunk.byteLength
|
|
68
|
+
}
|
|
69
|
+
return new TextDecoder().decode(body)
|
|
70
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbound webhook handler types (SPEC: 2026-03-23-inbound-webhook-handlers).
|
|
3
|
+
*
|
|
4
|
+
* These mirror the event-subscriber DX: a module exports `webhook-sources.ts`
|
|
5
|
+
* (one or more `WebhookSourceConfig`) and `webhook-handlers/*.ts` files that each
|
|
6
|
+
* export `metadata: WebhookHandlerMeta` plus a default handler function.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Raw inbound request passed to a source verifier. */
|
|
10
|
+
export interface InboundWebhookRequest {
|
|
11
|
+
/** Raw request body, required for signature verification. */
|
|
12
|
+
body: string
|
|
13
|
+
/** Lower-cased request headers. */
|
|
14
|
+
headers: Record<string, string>
|
|
15
|
+
/** Parsed request body (best-effort JSON). */
|
|
16
|
+
parsedBody: Record<string, unknown>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Optional descriptor of the credential fields a source needs (drives the admin UI). */
|
|
20
|
+
export interface WebhookSourceCredentialField {
|
|
21
|
+
key: string
|
|
22
|
+
label: string
|
|
23
|
+
secret?: boolean
|
|
24
|
+
required?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Configuration for an external webhook source (e.g. `stripe`, `resend`).
|
|
29
|
+
* Declared in a module's `webhook-sources.ts` via the `webhookSources` export.
|
|
30
|
+
*/
|
|
31
|
+
export interface WebhookSourceConfig {
|
|
32
|
+
/** Unique source identifier, e.g. `stripe`. Also the inbound path segment. */
|
|
33
|
+
key: string
|
|
34
|
+
/** Human-readable label. */
|
|
35
|
+
label: string
|
|
36
|
+
/**
|
|
37
|
+
* Verify the authenticity of an inbound webhook.
|
|
38
|
+
* Return true if valid, false to reject with 401. Throwing rejects with 400/500.
|
|
39
|
+
*/
|
|
40
|
+
verifier: (
|
|
41
|
+
request: InboundWebhookRequest,
|
|
42
|
+
credentials: Record<string, string>,
|
|
43
|
+
) => Promise<boolean>
|
|
44
|
+
/** Extract the event type from the parsed body/headers. */
|
|
45
|
+
eventTypeExtractor: (
|
|
46
|
+
body: Record<string, unknown>,
|
|
47
|
+
headers: Record<string, string>,
|
|
48
|
+
) => string
|
|
49
|
+
/** Extract a unique message id for deduplication. Return undefined if unavailable. */
|
|
50
|
+
messageIdExtractor?: (
|
|
51
|
+
body: Record<string, unknown>,
|
|
52
|
+
headers: Record<string, string>,
|
|
53
|
+
) => string | undefined
|
|
54
|
+
/** Optionally derive tenant scope from the payload/headers. */
|
|
55
|
+
scopeExtractor?: (
|
|
56
|
+
body: Record<string, unknown>,
|
|
57
|
+
headers: Record<string, string>,
|
|
58
|
+
) => { tenantId?: string; organizationId?: string }
|
|
59
|
+
/** Credential fields this source needs; used by the source-credential admin UI. */
|
|
60
|
+
credentialFields?: WebhookSourceCredentialField[]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Metadata exported by every `webhook-handlers/*.ts` file. */
|
|
64
|
+
export interface WebhookHandlerMeta {
|
|
65
|
+
/** Source key this handler listens to, e.g. `stripe`. */
|
|
66
|
+
source: string
|
|
67
|
+
/** Event-type pattern (supports wildcards: `*`, `payment_intent.*`). */
|
|
68
|
+
event: string
|
|
69
|
+
/** Unique handler id, e.g. `payments:stripe-payment-succeeded`. */
|
|
70
|
+
id: string
|
|
71
|
+
/** If true (default), handler runs via the queue-backed dispatcher. */
|
|
72
|
+
persistent?: boolean
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Payload handed to a webhook handler. */
|
|
76
|
+
export interface WebhookHandlerPayload {
|
|
77
|
+
/** Parsed webhook body. */
|
|
78
|
+
data: Record<string, unknown>
|
|
79
|
+
/** Extracted event type. */
|
|
80
|
+
eventType: string
|
|
81
|
+
/** Source key. */
|
|
82
|
+
sourceKey: string
|
|
83
|
+
/** Selected request headers. */
|
|
84
|
+
headers: Record<string, string>
|
|
85
|
+
/** WebhookIngestion record id. */
|
|
86
|
+
ingestionId: string
|
|
87
|
+
/** Tenant scope. */
|
|
88
|
+
tenantId: string
|
|
89
|
+
organizationId: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** DI accessor handed to a webhook handler. */
|
|
93
|
+
export interface WebhookHandlerContext {
|
|
94
|
+
resolve: <T>(name: string) => T
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A webhook handler function (default export of a `webhook-handlers/*.ts` file). */
|
|
98
|
+
export type WebhookHandler = (
|
|
99
|
+
payload: WebhookHandlerPayload,
|
|
100
|
+
ctx: WebhookHandlerContext,
|
|
101
|
+
) => Promise<void>
|
|
102
|
+
|
|
103
|
+
/** A generated registry entry binding handler metadata to a lazy module loader. */
|
|
104
|
+
export interface WebhookHandlerRegistryEntry {
|
|
105
|
+
meta: WebhookHandlerMeta
|
|
106
|
+
handler: () => Promise<{ default: WebhookHandler }>
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Per-handler execution detail recorded on a WebhookIngestion. */
|
|
110
|
+
export interface WebhookHandlerResult {
|
|
111
|
+
handlerId: string
|
|
112
|
+
module: string
|
|
113
|
+
status: 'success' | 'failed'
|
|
114
|
+
errorMessage?: string
|
|
115
|
+
durationMs: number
|
|
116
|
+
startedAt: string
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Lifecycle status of an inbound webhook ingestion. */
|
|
120
|
+
export type WebhookIngestionStatus =
|
|
121
|
+
| 'received'
|
|
122
|
+
| 'processing'
|
|
123
|
+
| 'processed'
|
|
124
|
+
| 'failed'
|
|
125
|
+
| 'duplicate'
|
|
@@ -1,4 +1,22 @@
|
|
|
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'
|
|
11
|
+
export type {
|
|
12
|
+
InboundWebhookRequest,
|
|
13
|
+
WebhookSourceCredentialField,
|
|
14
|
+
WebhookSourceConfig,
|
|
15
|
+
WebhookHandlerMeta,
|
|
16
|
+
WebhookHandlerPayload,
|
|
17
|
+
WebhookHandlerContext,
|
|
18
|
+
WebhookHandler,
|
|
19
|
+
WebhookHandlerRegistryEntry,
|
|
20
|
+
WebhookHandlerResult,
|
|
21
|
+
WebhookIngestionStatus,
|
|
22
|
+
} from './inbound-types'
|
|
@@ -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,183 @@
|
|
|
1
|
+
/** @jest-environment node */
|
|
2
|
+
|
|
3
|
+
// `defaultGroupOrder` in `auth/lib/backendChrome.tsx` ranks any group it lists ahead of any group it
|
|
4
|
+
// does not, regardless of an app's own page `priority`/`order` — so a downstream app could not place its
|
|
5
|
+
// own primary module above the shipped groups by any documented means. `overrides.nav.groupOrder` wires
|
|
6
|
+
// that as a real override domain, which `BACKWARD_COMPATIBILITY.md` §2 explicitly reserves for additive
|
|
7
|
+
// wiring.
|
|
8
|
+
|
|
9
|
+
const mockLoggerWarn = jest.fn()
|
|
10
|
+
|
|
11
|
+
jest.mock('../../lib/logger', () => ({
|
|
12
|
+
createLogger: () => {
|
|
13
|
+
const child = { warn: (...args: unknown[]) => mockLoggerWarn(...args), error: jest.fn(), info: jest.fn(), debug: jest.fn() }
|
|
14
|
+
return { child: () => child, warn: child.warn, error: child.error, info: child.info, debug: child.debug }
|
|
15
|
+
},
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
applyModuleOverridesFromEnabledModules,
|
|
20
|
+
applyNavGroupOrderOverrides,
|
|
21
|
+
getNavGroupOrderOverride,
|
|
22
|
+
resetModuleContractOverridesForTests,
|
|
23
|
+
resetModuleOverrideAppliersForTests,
|
|
24
|
+
} from '../overrides'
|
|
25
|
+
|
|
26
|
+
function reset() {
|
|
27
|
+
resetModuleOverrideAppliersForTests()
|
|
28
|
+
resetModuleContractOverridesForTests()
|
|
29
|
+
mockLoggerWarn.mockClear()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
beforeEach(reset)
|
|
33
|
+
afterEach(reset)
|
|
34
|
+
|
|
35
|
+
const collisionWarnings = () =>
|
|
36
|
+
mockLoggerWarn.mock.calls.filter((call) => String(call[0]).includes('nav.groupOrder declared by more than one'))
|
|
37
|
+
|
|
38
|
+
describe('nav.groupOrder override domain', () => {
|
|
39
|
+
it('is unset when no module declares it', () => {
|
|
40
|
+
applyModuleOverridesFromEnabledModules([{ id: 'catalog' }, { id: 'sales', overrides: {} }])
|
|
41
|
+
|
|
42
|
+
expect(getNavGroupOrderOverride()).toBeNull()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('captures the ids a module declares, in order — proving the domain is wired', () => {
|
|
46
|
+
applyModuleOverridesFromEnabledModules([
|
|
47
|
+
{ id: 'app', overrides: { nav: { groupOrder: ['app.nav.group', 'catalog.nav.group'] } } },
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
expect(getNavGroupOrderOverride()).toEqual(['app.nav.group', 'catalog.nav.group'])
|
|
51
|
+
// An unwired domain would be dropped with a "not yet wired" warning instead of being captured.
|
|
52
|
+
expect(mockLoggerWarn.mock.calls.filter((call) => String(call[0]).includes('not yet wired'))).toEqual([])
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('ignores an empty list, leaving the shipped ordering in charge', () => {
|
|
56
|
+
applyModuleOverridesFromEnabledModules([{ id: 'app', overrides: { nav: { groupOrder: [] } } }])
|
|
57
|
+
|
|
58
|
+
expect(getNavGroupOrderOverride()).toBeNull()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('drops blank entries and de-duplicates while preserving first position', () => {
|
|
62
|
+
applyModuleOverridesFromEnabledModules([
|
|
63
|
+
{
|
|
64
|
+
id: 'app',
|
|
65
|
+
overrides: { nav: { groupOrder: [' app.nav.group ', '', ' ', 'app.nav.group', 'catalog.nav.group'] } },
|
|
66
|
+
},
|
|
67
|
+
])
|
|
68
|
+
|
|
69
|
+
expect(getNavGroupOrderOverride()).toEqual(['app.nav.group', 'catalog.nav.group'])
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('ignores a non-array value rather than throwing', () => {
|
|
73
|
+
applyModuleOverridesFromEnabledModules([
|
|
74
|
+
{ id: 'app', overrides: { nav: { groupOrder: 'app.nav.group' as never } } },
|
|
75
|
+
])
|
|
76
|
+
|
|
77
|
+
expect(getNavGroupOrderOverride()).toBeNull()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('lets the later module win when two declare an order, and says so', () => {
|
|
81
|
+
applyModuleOverridesFromEnabledModules([
|
|
82
|
+
{ id: 'first', overrides: { nav: { groupOrder: ['first.nav.group'] } } },
|
|
83
|
+
{ id: 'second', overrides: { nav: { groupOrder: ['second.nav.group'] } } },
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
expect(getNavGroupOrderOverride()).toEqual(['second.nav.group'])
|
|
87
|
+
expect(collisionWarnings()).toHaveLength(1)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('does not warn when the same module declares it twice', () => {
|
|
91
|
+
applyModuleOverridesFromEnabledModules([
|
|
92
|
+
{ id: 'app', overrides: { nav: { groupOrder: ['a.nav.group'] } } },
|
|
93
|
+
{ id: 'app', overrides: { nav: { groupOrder: ['b.nav.group'] } } },
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
expect(getNavGroupOrderOverride()).toEqual(['b.nav.group'])
|
|
97
|
+
expect(collisionWarnings()).toEqual([])
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('is cleared by the store reset hook so suites cannot leak into each other', () => {
|
|
101
|
+
applyModuleOverridesFromEnabledModules([
|
|
102
|
+
{ id: 'app', overrides: { nav: { groupOrder: ['app.nav.group'] } } },
|
|
103
|
+
])
|
|
104
|
+
expect(getNavGroupOrderOverride()).not.toBeNull()
|
|
105
|
+
|
|
106
|
+
resetModuleContractOverridesForTests()
|
|
107
|
+
|
|
108
|
+
expect(getNavGroupOrderOverride()).toBeNull()
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe('nav.groupOrder programmatic tier', () => {
|
|
113
|
+
it('takes precedence over the modules.ts declaration', () => {
|
|
114
|
+
applyModuleOverridesFromEnabledModules([
|
|
115
|
+
{ id: 'app', overrides: { nav: { groupOrder: ['from-modules.nav.group'] } } },
|
|
116
|
+
])
|
|
117
|
+
applyNavGroupOrderOverrides(['from-code.nav.group'])
|
|
118
|
+
|
|
119
|
+
expect(getNavGroupOrderOverride()).toEqual(['from-code.nav.group'])
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('falls back to the modules.ts declaration when cleared with null', () => {
|
|
123
|
+
applyModuleOverridesFromEnabledModules([
|
|
124
|
+
{ id: 'app', overrides: { nav: { groupOrder: ['from-modules.nav.group'] } } },
|
|
125
|
+
])
|
|
126
|
+
applyNavGroupOrderOverrides(['from-code.nav.group'])
|
|
127
|
+
applyNavGroupOrderOverrides(null)
|
|
128
|
+
|
|
129
|
+
expect(getNavGroupOrderOverride()).toEqual(['from-modules.nav.group'])
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('applies the same normalisation as the modules.ts tier', () => {
|
|
133
|
+
applyNavGroupOrderOverrides([' a.nav.group ', '', 'a.nav.group', 'b.nav.group'])
|
|
134
|
+
|
|
135
|
+
expect(getNavGroupOrderOverride()).toEqual(['a.nav.group', 'b.nav.group'])
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('treats an all-blank list as no override', () => {
|
|
139
|
+
applyNavGroupOrderOverrides([' ', ''])
|
|
140
|
+
|
|
141
|
+
expect(getNavGroupOrderOverride()).toBeNull()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('works before any module override has been dispatched', () => {
|
|
145
|
+
applyNavGroupOrderOverrides(['standalone.nav.group'])
|
|
146
|
+
|
|
147
|
+
expect(getNavGroupOrderOverride()).toEqual(['standalone.nav.group'])
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
describe('nav.groupOrder survives module duplication', () => {
|
|
152
|
+
// Regression for `.ai/lessons.md`, "Global registries in publishable packages must use
|
|
153
|
+
// `globalThis`". This domain's writer (app bootstrap) and reader (`@open-mercato/core`'s backend
|
|
154
|
+
// chrome) sit in different packages, and standalone builds can evaluate `@open-mercato/shared`
|
|
155
|
+
// through more than one module instance. `jest.isolateModules` reproduces that: a second, freshly
|
|
156
|
+
// loaded copy of the module must still observe what the first copy stored.
|
|
157
|
+
it('a separately loaded module instance sees the value written by bootstrap', () => {
|
|
158
|
+
applyModuleOverridesFromEnabledModules([
|
|
159
|
+
{ id: 'app', overrides: { nav: { groupOrder: ['bootstrap.nav.group'] } } },
|
|
160
|
+
])
|
|
161
|
+
|
|
162
|
+
let observedFromSecondInstance: readonly string[] | null | undefined
|
|
163
|
+
jest.isolateModules(() => {
|
|
164
|
+
const freshModule = require('../overrides') as typeof import('../overrides')
|
|
165
|
+
observedFromSecondInstance = freshModule.getNavGroupOrderOverride()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// A module-local variable would leave this second instance blind to the bootstrap value.
|
|
169
|
+
expect(observedFromSecondInstance).toEqual(['bootstrap.nav.group'])
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('a programmatic override is visible to a separately loaded instance too', () => {
|
|
173
|
+
applyNavGroupOrderOverrides(['programmatic.nav.group'])
|
|
174
|
+
|
|
175
|
+
let observed: readonly string[] | null | undefined
|
|
176
|
+
jest.isolateModules(() => {
|
|
177
|
+
const freshModule = require('../overrides') as typeof import('../overrides')
|
|
178
|
+
observed = freshModule.getNavGroupOrderOverride()
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
expect(observed).toEqual(['programmatic.nav.group'])
|
|
182
|
+
})
|
|
183
|
+
})
|
|
@@ -94,7 +94,11 @@ describe('CLI Modules Registry', () => {
|
|
|
94
94
|
{
|
|
95
95
|
id: 'auth',
|
|
96
96
|
defaultEncryptionMaps: [
|
|
97
|
-
{
|
|
97
|
+
{
|
|
98
|
+
entityId: 'auth:user',
|
|
99
|
+
keyScope: 'system',
|
|
100
|
+
fields: [{ field: 'email', hashField: 'email_hash' }],
|
|
101
|
+
},
|
|
98
102
|
],
|
|
99
103
|
},
|
|
100
104
|
{
|
|
@@ -106,7 +110,11 @@ describe('CLI Modules Registry', () => {
|
|
|
106
110
|
])
|
|
107
111
|
|
|
108
112
|
expect(maps).toEqual([
|
|
109
|
-
{
|
|
113
|
+
{
|
|
114
|
+
entityId: 'auth:user',
|
|
115
|
+
keyScope: 'system',
|
|
116
|
+
fields: [{ field: 'email', hashField: 'email_hash' }],
|
|
117
|
+
},
|
|
110
118
|
{ entityId: 'customers:customer_comment', fields: [{ field: 'body', hashField: null }] },
|
|
111
119
|
])
|
|
112
120
|
})
|
package/src/modules/analytics.ts
CHANGED
|
@@ -13,6 +13,13 @@ export type AnalyticsEntityTypeConfig = {
|
|
|
13
13
|
schema?: string
|
|
14
14
|
dateField: string
|
|
15
15
|
defaultScopeFields: string[]
|
|
16
|
+
/**
|
|
17
|
+
* Field name (a key of `fieldMappings`) holding the per-row currency of monetary
|
|
18
|
+
* columns. Declaring it lets analytics consumers verify that the rows they aggregate
|
|
19
|
+
* all carry one currency before labelling the result with it (#4676). Omit it for
|
|
20
|
+
* entities whose amounts are not per-row denominated.
|
|
21
|
+
*/
|
|
22
|
+
currencyField?: string
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
export type AnalyticsFieldMapping = {
|
|
@@ -3,7 +3,10 @@ export type ModuleEncryptionFieldRule = {
|
|
|
3
3
|
hashField?: string | null
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
export type EncryptionKeyScope = 'tenant' | 'system'
|
|
7
|
+
|
|
6
8
|
export type ModuleEncryptionMap = {
|
|
7
9
|
entityId: string
|
|
10
|
+
keyScope?: EncryptionKeyScope
|
|
8
11
|
fields: ModuleEncryptionFieldRule[]
|
|
9
12
|
}
|
|
@@ -44,9 +44,23 @@ export type BackendChromeBrand = {
|
|
|
44
44
|
logo?: {
|
|
45
45
|
src: string
|
|
46
46
|
alt?: string
|
|
47
|
+
preserveAspectRatio?: boolean
|
|
47
48
|
} | null
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
/**
|
|
52
|
+
* The organization the current request is scoped to, resolved server-side.
|
|
53
|
+
*
|
|
54
|
+
* Distinct from `brand`, which is a *branding* channel and only populates when the organization has a
|
|
55
|
+
* logo configured. This is always present when a single organization is in scope, so UI can label
|
|
56
|
+
* "you are viewing: <name>" without a second round trip. `null` under an all-organizations selection,
|
|
57
|
+
* when no organization is in scope, or when the lookup fails.
|
|
58
|
+
*/
|
|
59
|
+
export type BackendChromeCurrentOrganization = {
|
|
60
|
+
id: string
|
|
61
|
+
name: string
|
|
62
|
+
}
|
|
63
|
+
|
|
50
64
|
export type BackendChromePayload = {
|
|
51
65
|
groups: BackendChromeNavGroup[]
|
|
52
66
|
settingsSections: BackendChromeSectionGroup[]
|
|
@@ -56,4 +70,5 @@ export type BackendChromePayload = {
|
|
|
56
70
|
grantedFeatures: string[]
|
|
57
71
|
roles: string[]
|
|
58
72
|
brand?: BackendChromeBrand | null
|
|
73
|
+
currentOrganization?: BackendChromeCurrentOrganization | null
|
|
59
74
|
}
|