@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
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:shared] found
|
|
1
|
+
[build:shared] found 257 entry points
|
|
2
2
|
[build:shared] built successfully
|
package/AGENTS.md
CHANGED
|
@@ -34,6 +34,7 @@ yarn workspace @open-mercato/shared build
|
|
|
34
34
|
|-----------|-------------|-------------|
|
|
35
35
|
| `api/` | When building scoped API payloads | `@open-mercato/shared/lib/api/scoped` |
|
|
36
36
|
| `auth/` | When you need wildcard-aware feature matching or shared auth helpers | `@open-mercato/shared/lib/auth/featureMatch` |
|
|
37
|
+
| `auth/organizationScope` | When an organization-scoped API route must resolve the caller's organization — falls back to `actorOrgId` for an "all organizations" selection, but only while the effective tenant is still the actor's tenant. On `null` for an authenticated caller answer with `organizationScopeRequiredResponse()` (400, code `organization_scope_required`) — never 401 | `@open-mercato/shared/lib/auth/organizationScope` — `resolveActiveOrganizationId(auth)`, `organizationScopeRequiredResponse()` |
|
|
37
38
|
| `boolean/` | When parsing boolean strings from env/query params | `@open-mercato/shared/lib/boolean` |
|
|
38
39
|
| `browser/` | When persisting client UI state to `localStorage` — use the safe wrappers and the versioned-envelope helper instead of raw `localStorage` reads/writes | `@open-mercato/shared/lib/browser/safeLocalStorage`, `@open-mercato/shared/lib/browser/versionedPreference` |
|
|
39
40
|
| `commands/` | When implementing undo/redo command pattern | `@open-mercato/shared/lib/commands` |
|
|
@@ -43,15 +44,16 @@ yarn workspace @open-mercato/shared build
|
|
|
43
44
|
| `custom-fields/` | When handling custom field payloads | `@open-mercato/shared/lib/custom-fields` |
|
|
44
45
|
| `data/` | When you need `DataEngine` or `QueryEngine` types | `@open-mercato/shared/lib/data/engine` |
|
|
45
46
|
| `db/` | When resolving the ORM/connection-pool config (`resolvePoolConfig`, pool/timeout env knobs) | `@open-mercato/shared/lib/db/mikro` |
|
|
46
|
-
| `di/` | When setting up dependency injection (Awilix) | `@open-mercato/shared/lib/di` |
|
|
47
|
+
| `di/` | When setting up dependency injection (Awilix). The app-level hook (`src/di.ts` → `register`) is wired explicitly in BOTH bootstrap paths — `src/bootstrap.ts` for the Next.js runtime, `bootstrapFromAppRoot()` for worker/scheduler/CLI processes. Never rely on the legacy `import('@/di')` fallback: the alias does not exist outside the app bundler | `@open-mercato/shared/lib/di` |
|
|
47
48
|
| `encryption/` | When querying encrypted entities (MUST use instead of raw `em.find`) | `@open-mercato/shared/lib/encryption/find` |
|
|
48
49
|
| `i18n/` | When translating strings — `useT()` client-side, `resolveTranslations()` server-side | `@open-mercato/shared/lib/i18n/context` or `/server` |
|
|
49
50
|
| `indexers/` | When building query index helpers | `@open-mercato/shared/lib/indexers` |
|
|
50
51
|
| `logger/` | When emitting diagnostics — `createLogger(namespace)` instead of raw `console.*` (migrate incrementally, Boy Scout rule). Message-first with structured fields (`logger.warn('Payload too large', { event, maxBytes })`), errors under `err`, `child(bindings)` for context, `getLogLevel()`/`isLevelEnabled()` to gate expensive fields; level via `OM_LOG_LEVEL`. Never log credentials, PII, or payload bodies | `@open-mercato/shared/lib/logger` |
|
|
51
|
-
| `modules/` | When registering or listing modules | `@open-mercato/shared/lib/modules/registry` |
|
|
52
|
+
| `modules/` | When registering or listing modules; `surfaceFingerprint` gives a deploy-time hash of the enabled modules, their declared ACL features, and the backend route manifest — mix it into any cache key whose payload is derived from those (no DB write exists to tag-invalidate on, so an omitted fingerprint serves the pre-deploy payload forever). It cannot see React-element fields such as a route `icon`, so callers MUST still pass a `ttl` | `@open-mercato/shared/lib/modules/registry`, `@open-mercato/shared/lib/modules/surfaceFingerprint` |
|
|
52
53
|
| `number.ts` | When parsing numeric strings from env/query params with a fallback and optional min/integer constraint | `@open-mercato/shared/lib/number` |
|
|
53
54
|
| `openapi/` | When generating CRUD OpenAPI specs | `@open-mercato/shared/lib/openapi/crud` |
|
|
54
55
|
| `profiler/` | When profiling with `OM_PROFILE` env flag | `@open-mercato/shared/lib/profiler` |
|
|
56
|
+
| `search/` | When resolving record ids from the `search_tokens` index — MUST use instead of hand-rolling the Kysely lookup, and MUST be unioned into (or replace) any `$ilike` filter on a column an encryption map covers | `@open-mercato/shared/lib/search/tokenLookup` |
|
|
55
57
|
| `string.ts` | When parsing comma-separated lists from CLI args/query params, or coercing a string to `undefined` when blank | `@open-mercato/shared/lib/string` |
|
|
56
58
|
| `testing/` | When bootstrapping tests — register only what the test needs | `@open-mercato/shared/lib/testing/bootstrap` |
|
|
57
59
|
|
|
@@ -89,6 +91,37 @@ import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/
|
|
|
89
91
|
const results = await findWithDecryption(em, 'Entity', filter, { tenantId, organizationId })
|
|
90
92
|
```
|
|
91
93
|
|
|
94
|
+
Encryption maps default to tenant-scoped keys. Use the additive `keyScope: 'system'` option only for records that must exist before a tenant does; the system scope remains authoritative after a tenant id is later assigned so existing ciphertext stays readable.
|
|
95
|
+
|
|
96
|
+
### Search Tokens — MUST use instead of `$ilike` on encrypted columns
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { findEntityIdsBySearchTokens } from '@open-mercato/shared/lib/search/tokenLookup'
|
|
100
|
+
|
|
101
|
+
const match = await findEntityIdsBySearchTokens({
|
|
102
|
+
db: em.getKysely<any>(),
|
|
103
|
+
entityType: E.customers.customer_entity,
|
|
104
|
+
query: term,
|
|
105
|
+
fields: ['display_name', 'primary_email'],
|
|
106
|
+
scope: { tenantId, organizationId },
|
|
107
|
+
})
|
|
108
|
+
if (match.matched && match.ids.length) filters.$or.push({ id: { $in: match.ids } })
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
An `$ilike` predicate runs against the stored column value. For a field covered by a
|
|
112
|
+
module encryption map that value is ciphertext, so the filter matches nothing and the
|
|
113
|
+
endpoint returns an empty page indistinguishable from a genuine no-result. The token
|
|
114
|
+
index stores hashes of the plaintext, so it keeps matching. Issue #2990.
|
|
115
|
+
|
|
116
|
+
- `matched: false` means the index was **not consulted** (blank query, `OM_SEARCH_ENABLED=off`,
|
|
117
|
+
or the term produced no tokens) — it is NOT "nothing matched". Keep the caller's own
|
|
118
|
+
predicate in that case.
|
|
119
|
+
- `matched: true` with `ids: []` is a real empty result.
|
|
120
|
+
- Queries that go through the query engine get this routing automatically; raw
|
|
121
|
+
`em.find` / Kysely list routes must wire it themselves. When the fallback would run
|
|
122
|
+
`ILIKE` against an encrypted column, both query engines now log a warning
|
|
123
|
+
(`lib/query/ciphertext-search-warning`) instead of degrading silently.
|
|
124
|
+
|
|
92
125
|
### Boolean Parsing — MUST use instead of ad-hoc parsing
|
|
93
126
|
|
|
94
127
|
```typescript
|
|
@@ -156,9 +189,23 @@ The detection scripts (`yarn i18n:check-hardcoded`, `yarn i18n:check-values`) li
|
|
|
156
189
|
import { withScopedPayload, createScopedApiHelpers } from '@open-mercato/shared/lib/api/scoped'
|
|
157
190
|
```
|
|
158
191
|
|
|
159
|
-
### Feature
|
|
192
|
+
### Feature Policy and Matching
|
|
193
|
+
|
|
194
|
+
Server authorization MUST use the consolidated policy:
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import {
|
|
198
|
+
authorizeFeatures,
|
|
199
|
+
resolveEffectiveFeatures,
|
|
200
|
+
} from '@open-mercato/shared/security/featurePolicy'
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
- Prefer the realm service (`rbacService.userHasAllFeatures` or `customerRbacService.userHasAllFeatures`) when a user and scope are available.
|
|
204
|
+
- Use `authorizeFeatures(required, subject)` only when the caller already has an ACL snapshot. It owns removed-feature, disabled-module, unrestricted-user, scope, and wildcard ordering.
|
|
205
|
+
- Use `resolveEffectiveFeatures(grants)` for browser capability payloads. It returns concrete IDs and never wildcards.
|
|
206
|
+
- Raw `loadAcl` / `getGrantedFeatures` remain valid for ACL management and infrastructure inspection, not as authorization entrypoints.
|
|
160
207
|
|
|
161
|
-
|
|
208
|
+
The low-level helpers remain pure for browser checks over effective projections and isolated grant-matching utilities:
|
|
162
209
|
|
|
163
210
|
```typescript
|
|
164
211
|
import { hasFeature, hasAllFeatures } from '@open-mercato/shared/security/features'
|
|
@@ -166,7 +213,8 @@ import { hasFeature, hasAllFeatures } from '@open-mercato/shared/security/featur
|
|
|
166
213
|
|
|
167
214
|
- Use `hasFeature(granted, 'module.action')` for single-feature checks.
|
|
168
215
|
- Use `hasAllFeatures(granted, required)` for arrays such as `features`, `requireFeatures`, or handler guard lists.
|
|
169
|
-
- MUST NOT
|
|
216
|
+
- MUST NOT use these low-level helpers as server authorization entrypoints.
|
|
217
|
+
- MUST NOT gate feature arrays with `includes(...)`, `Set.has(...)`, or ad hoc `every(...includes(...))` checks.
|
|
170
218
|
|
|
171
219
|
### CRUD HTTP Errors — MUST use the shared helpers instead of hand-rolling `CrudHttpError`
|
|
172
220
|
|
|
@@ -208,7 +256,7 @@ MUST rules:
|
|
|
208
256
|
|
|
209
257
|
### Module-Level Overrides (`@open-mercato/shared/modules/overrides`)
|
|
210
258
|
|
|
211
|
-
Downstream apps replace or disable any contract a module presents through a single `entry.overrides` field on a `ModuleEntry`. The umbrella spec is `.ai/specs/implemented/2026-05-04-modules-ts-unified-overrides.md`; phases 1-
|
|
259
|
+
Downstream apps replace or disable any contract a module presents through a single `entry.overrides` field on a `ModuleEntry`. The umbrella spec is `.ai/specs/implemented/2026-05-04-modules-ts-unified-overrides.md`; phases 1-19 are wired.
|
|
212
260
|
|
|
213
261
|
| Use case | Helper |
|
|
214
262
|
|----------|--------|
|
|
@@ -220,6 +268,7 @@ Downstream apps replace or disable any contract a module presents through a sing
|
|
|
220
268
|
| Widgets | `applyInjectionWidgetOverridesToEntries()`, `applyInjectionWidgetOverridesToTables()`, `applyDashboardWidgetOverridesToEntries()`, `applyComponentOverridesToEntries()` |
|
|
221
269
|
| Notifications / interceptors / enrichers / guards | `applyNotificationTypeOverridesToEntries()`, `applyNotificationHandlerOverridesToEntries()`, `applyApiInterceptorOverridesToEntries()`, `applyCommandInterceptorOverridesToEntries()`, `applyResponseEnricherOverridesToEntries()`, `applyPageGuardOverridesToEntries()` |
|
|
222
270
|
| DI | `applyDiOverridesToContainer()` |
|
|
271
|
+
| Sidebar nav ordering | `applyNavGroupOrderOverrides()`, `getNavGroupOrderOverride()` |
|
|
223
272
|
|
|
224
273
|
MUST rules:
|
|
225
274
|
- `entry.overrides` is the ONLY canonical override surface — never patch upstream module source.
|
|
@@ -228,6 +277,8 @@ MUST rules:
|
|
|
228
277
|
- `null` disables the matching method; `{ handler, metadata? }` replaces it. Disabling every method on an entry drops the entry.
|
|
229
278
|
- The dispatcher SHOULD run from `bootstrap.ts` BEFORE any registry first-loads (`registerApiRouteManifests`, widget registries, notification registries, etc.) so the overrides take effect when the registry stores entries.
|
|
230
279
|
- Adding a new override domain MUST follow the umbrella spec: typed sub-shape + composer + runtime hook + tests + AGENTS.md/docs update + status-table tick.
|
|
280
|
+
- `nav.groupOrder` **prepends** group ids ahead of the built-in ordering; ids it does not name keep their current position. It is a default, resolved beneath role and per-user sidebar preferences, and an absent override MUST leave ordering byte-identical.
|
|
281
|
+
- Nav ordering state lives on `globalThis` because its reader is `@open-mercato/core` while its writer is app bootstrap; a module-local variable would be invisible across duplicated module instances in standalone builds.
|
|
231
282
|
|
|
232
283
|
### Query Engine Extensibility (UMES)
|
|
233
284
|
|
package/build.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs'
|
|
|
2
2
|
import { dirname, join } from 'node:path'
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
4
|
import { buildPackage } from '../../scripts/build-package.mjs'
|
|
5
|
+
import { buildVersionSource } from './scripts/versionSource.cjs'
|
|
5
6
|
|
|
6
7
|
const packageDir = dirname(fileURLToPath(import.meta.url))
|
|
7
8
|
const packageJson = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf-8'))
|
|
@@ -12,10 +13,7 @@ const injectVersion = {
|
|
|
12
13
|
name: 'inject-version',
|
|
13
14
|
setup(build) {
|
|
14
15
|
build.onLoad({ filter: /lib\/version\.ts$/ }, async () => ({
|
|
15
|
-
contents:
|
|
16
|
-
export const APP_VERSION = '${packageVersion}'
|
|
17
|
-
export const appVersion = APP_VERSION
|
|
18
|
-
`,
|
|
16
|
+
contents: buildVersionSource(packageVersion),
|
|
19
17
|
loader: 'ts',
|
|
20
18
|
}))
|
|
21
19
|
},
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
const SAFETY_IDENTIFIER_SECRET_LABEL = "open-mercato:ai-safety-identifier:v1";
|
|
3
|
+
const derivedSecretCache = /* @__PURE__ */ new Map();
|
|
4
|
+
function readBaseSecret(explicit) {
|
|
5
|
+
const secret = explicit ?? process.env.JWT_SECRET;
|
|
6
|
+
if (!secret) {
|
|
7
|
+
throw new Error("[internal] JWT_SECRET is not set; cannot derive AI safety-identifier secret");
|
|
8
|
+
}
|
|
9
|
+
return secret;
|
|
10
|
+
}
|
|
11
|
+
function deriveAiSafetyIdentifierSecret(baseSecret) {
|
|
12
|
+
const base = readBaseSecret(baseSecret);
|
|
13
|
+
const cached = derivedSecretCache.get(base);
|
|
14
|
+
if (cached !== void 0) return cached;
|
|
15
|
+
const derived = crypto.createHmac("sha256", base).update(SAFETY_IDENTIFIER_SECRET_LABEL).digest("hex");
|
|
16
|
+
derivedSecretCache.set(base, derived);
|
|
17
|
+
return derived;
|
|
18
|
+
}
|
|
19
|
+
function computeEndUserIdentifier(tenantId, userId, options) {
|
|
20
|
+
const normalizedUser = (userId ?? "").trim();
|
|
21
|
+
if (!normalizedUser) {
|
|
22
|
+
throw new Error("[internal] computeEndUserIdentifier requires a non-empty userId");
|
|
23
|
+
}
|
|
24
|
+
const key = deriveAiSafetyIdentifierSecret(options?.baseSecret);
|
|
25
|
+
const salt = (tenantId ?? "").trim();
|
|
26
|
+
return crypto.createHmac("sha256", key).update(`${salt}:${normalizedUser}`).digest("hex");
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
computeEndUserIdentifier,
|
|
30
|
+
deriveAiSafetyIdentifierSecret
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=safety-identifier.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/ai/safety-identifier.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * End-user safety identifiers for AI provider calls.\n *\n * Providers such as OpenAI let developers attach an opaque per-end-user\n * identifier to each request so abuse enforcement can target one user instead\n * of suspending the whole API-key organization. We never send a reversible id:\n * the value is a tenant-salted HMAC computed from the platform's existing auth\n * secret, so no PII or internal id leaves the platform and the same end user in\n * two tenants produces two unrelated hashes.\n *\n * The per-process secret derivation mirrors `deriveJwtAudienceSecret`\n * (`@open-mercato/shared/lib/auth/jwt`): one HMAC from the base `JWT_SECRET`\n * under a versioned purpose label, memoized for the process lifetime. No new\n * secret to provision.\n *\n * @see .ai/specs/2026-06-04-ai-input-moderation-and-safety-identifiers.md\n */\n\nimport crypto from 'node:crypto'\n\nconst SAFETY_IDENTIFIER_SECRET_LABEL = 'open-mercato:ai-safety-identifier:v1'\n\nconst derivedSecretCache = new Map<string, string>()\n\nfunction readBaseSecret(explicit?: string): string {\n const secret = explicit ?? process.env.JWT_SECRET\n if (!secret) {\n throw new Error('[internal] JWT_SECRET is not set; cannot derive AI safety-identifier secret')\n }\n return secret\n}\n\n/**\n * Derive the per-process safety-identifier HMAC key from the base auth secret.\n *\n * Deterministic HMAC-SHA256 of a versioned purpose label keyed by the base\n * secret, memoized per base secret. Rotating the base secret rotates every\n * derived identifier \u2014 documented as accepted (identifiers are advisory\n * provider-side metadata, not an in-platform security control).\n */\nexport function deriveAiSafetyIdentifierSecret(baseSecret?: string): string {\n const base = readBaseSecret(baseSecret)\n const cached = derivedSecretCache.get(base)\n if (cached !== undefined) return cached\n const derived = crypto\n .createHmac('sha256', base)\n .update(SAFETY_IDENTIFIER_SECRET_LABEL)\n .digest('hex')\n derivedSecretCache.set(base, derived)\n return derived\n}\n\n/**\n * Compute the opaque end-user safety identifier for a (tenant, user) pair.\n *\n * Returns a 64-char lowercase hex HMAC-SHA256 of `${tenantId}:${userId}` keyed\n * by the derived secret. Throws (with an `[internal]` message) when the base\n * secret is missing or `userId` is empty \u2014 callers in the runtime wrap this in\n * a best-effort try/catch so identifier-derivation failures never break chat.\n */\nexport function computeEndUserIdentifier(\n tenantId: string | null | undefined,\n userId: string,\n options?: { baseSecret?: string },\n): string {\n const normalizedUser = (userId ?? '').trim()\n if (!normalizedUser) {\n throw new Error('[internal] computeEndUserIdentifier requires a non-empty userId')\n }\n const key = deriveAiSafetyIdentifierSecret(options?.baseSecret)\n const salt = (tenantId ?? '').trim()\n return crypto.createHmac('sha256', key).update(`${salt}:${normalizedUser}`).digest('hex')\n}\n"],
|
|
5
|
+
"mappings": "AAkBA,OAAO,YAAY;AAEnB,MAAM,iCAAiC;AAEvC,MAAM,qBAAqB,oBAAI,IAAoB;AAEnD,SAAS,eAAe,UAA2B;AACjD,QAAM,SAAS,YAAY,QAAQ,IAAI;AACvC,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,6EAA6E;AAAA,EAC/F;AACA,SAAO;AACT;AAUO,SAAS,+BAA+B,YAA6B;AAC1E,QAAM,OAAO,eAAe,UAAU;AACtC,QAAM,SAAS,mBAAmB,IAAI,IAAI;AAC1C,MAAI,WAAW,OAAW,QAAO;AACjC,QAAM,UAAU,OACb,WAAW,UAAU,IAAI,EACzB,OAAO,8BAA8B,EACrC,OAAO,KAAK;AACf,qBAAmB,IAAI,MAAM,OAAO;AACpC,SAAO;AACT;AAUO,SAAS,yBACd,UACA,QACA,SACQ;AACR,QAAM,kBAAkB,UAAU,IAAI,KAAK;AAC3C,MAAI,CAAC,gBAAgB;AACnB,UAAM,IAAI,MAAM,iEAAiE;AAAA,EACnF;AACA,QAAM,MAAM,+BAA+B,SAAS,UAAU;AAC9D,QAAM,QAAQ,YAAY,IAAI,KAAK;AACnC,SAAO,OAAO,WAAW,UAAU,GAAG,EAAE,OAAO,GAAG,IAAI,IAAI,cAAc,EAAE,EAAE,OAAO,KAAK;AAC1F;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function normalizeId(value) {
|
|
2
|
+
if (typeof value !== "string") return null;
|
|
3
|
+
const trimmed = value.trim();
|
|
4
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
5
|
+
}
|
|
6
|
+
function resolveActiveOrganizationId(auth) {
|
|
7
|
+
if (!auth) return null;
|
|
8
|
+
const selected = normalizeId(auth.orgId);
|
|
9
|
+
if (selected) return selected;
|
|
10
|
+
const actorOrgId = normalizeId(auth.actorOrgId);
|
|
11
|
+
if (!actorOrgId) return null;
|
|
12
|
+
if ("actorTenantId" in auth) {
|
|
13
|
+
const actorTenantId = normalizeId(auth.actorTenantId);
|
|
14
|
+
const effectiveTenantId = normalizeId(auth.tenantId);
|
|
15
|
+
if (!actorTenantId || actorTenantId !== effectiveTenantId) return null;
|
|
16
|
+
}
|
|
17
|
+
return actorOrgId;
|
|
18
|
+
}
|
|
19
|
+
const ORGANIZATION_SCOPE_REQUIRED_ERROR_CODE = "organization_scope_required";
|
|
20
|
+
function organizationScopeRequiredResponse() {
|
|
21
|
+
return Response.json(
|
|
22
|
+
{
|
|
23
|
+
error: "Select an organization to access this resource",
|
|
24
|
+
code: ORGANIZATION_SCOPE_REQUIRED_ERROR_CODE
|
|
25
|
+
},
|
|
26
|
+
{ status: 400 }
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
ORGANIZATION_SCOPE_REQUIRED_ERROR_CODE,
|
|
31
|
+
organizationScopeRequiredResponse,
|
|
32
|
+
resolveActiveOrganizationId
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=organizationScope.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/auth/organizationScope.ts"],
|
|
4
|
+
"sourcesContent": ["type OrganizationScopedAuth = {\n orgId?: string | null\n actorOrgId?: unknown\n tenantId?: string | null\n actorTenantId?: unknown\n} | null | undefined\n\nfunction normalizeId(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\n/**\n * Resolves the organization a request is scoped to when the caller may be viewing\n * \"all organizations\".\n *\n * Organization-scoped configuration modules (integrations credentials/state, data sync\n * mappings/schedules/runs) all require a non-null `organization_id`, so there is no\n * meaningful \"all organizations\" view of them. When an operator selects that option the\n * super-admin cookie override clears `auth.orgId` and preserves the actor's own\n * organization in `actorOrgId`; fall back to it so those modules keep showing the\n * operator's own configuration instead of failing.\n *\n * The fallback is only valid while the effective tenant is still the actor's own tenant.\n * When the super-admin cookie override also switched tenants (`actorTenantId` is present\n * and differs from `auth.tenantId`), the actor's organization belongs to another tenant \u2014\n * scoping to it would persist a cross-tenant `{ organizationId, tenantId }` pair. Return\n * `null` instead and let the route answer with `organizationScopeRequiredResponse()`.\n *\n * Answering 401 for an unresolvable scope is not merely wrong but self-perpetuating:\n * `apiFetch` reads 401 as an expired session and redirects through\n * `/api/auth/session/refresh`, which succeeds and returns to the same page, reloading\n * forever. That is why the missing-scope answer is a 400, never a 401.\n */\nexport function resolveActiveOrganizationId(auth: OrganizationScopedAuth): string | null {\n if (!auth) return null\n const selected = normalizeId(auth.orgId)\n if (selected) return selected\n const actorOrgId = normalizeId(auth.actorOrgId)\n if (!actorOrgId) return null\n if ('actorTenantId' in auth) {\n const actorTenantId = normalizeId(auth.actorTenantId)\n const effectiveTenantId = normalizeId(auth.tenantId)\n if (!actorTenantId || actorTenantId !== effectiveTenantId) return null\n }\n return actorOrgId\n}\n\nexport const ORGANIZATION_SCOPE_REQUIRED_ERROR_CODE = 'organization_scope_required'\n\n/**\n * 400 response for an authenticated caller whose organization scope cannot be resolved\n * (e.g. a super-admin viewing a foreign tenant with \"all organizations\" selected).\n * Deliberately not a 401: the session is valid, so refreshing it would loop.\n */\nexport function organizationScopeRequiredResponse(): Response {\n return Response.json(\n {\n error: 'Select an organization to access this resource',\n code: ORGANIZATION_SCOPE_REQUIRED_ERROR_CODE,\n },\n { status: 400 },\n )\n}\n"],
|
|
5
|
+
"mappings": "AAOA,SAAS,YAAY,OAA+B;AAClD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK;AAC3B,SAAO,QAAQ,SAAS,IAAI,UAAU;AACxC;AAwBO,SAAS,4BAA4B,MAA6C;AACvF,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,WAAW,YAAY,KAAK,KAAK;AACvC,MAAI,SAAU,QAAO;AACrB,QAAM,aAAa,YAAY,KAAK,UAAU;AAC9C,MAAI,CAAC,WAAY,QAAO;AACxB,MAAI,mBAAmB,MAAM;AAC3B,UAAM,gBAAgB,YAAY,KAAK,aAAa;AACpD,UAAM,oBAAoB,YAAY,KAAK,QAAQ;AACnD,QAAI,CAAC,iBAAiB,kBAAkB,kBAAmB,QAAO;AAAA,EACpE;AACA,SAAO;AACT;AAEO,MAAM,yCAAyC;AAO/C,SAAS,oCAA8C;AAC5D,SAAO,SAAS;AAAA,IACd;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,IACR;AAAA,IACA,EAAE,QAAQ,IAAI;AAAA,EAChB;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/lib/auth/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { cookies } from "next/headers.js";
|
|
2
2
|
import { verifyJwt } from "./jwt.js";
|
|
3
3
|
import { getSharedApiKeyAuthCache } from "./apiKeyAuthCache.js";
|
|
4
|
+
import { isTransientDbError } from "@open-mercato/shared/lib/db/pg-errors";
|
|
4
5
|
const TENANT_COOKIE_NAME = "om_selected_tenant";
|
|
5
6
|
const ORGANIZATION_COOKIE_NAME = "om_selected_org";
|
|
6
7
|
const ALL_ORGANIZATIONS_COOKIE_VALUE = "__all__";
|
|
@@ -175,7 +176,10 @@ async function resolveApiKeyAuth(secret) {
|
|
|
175
176
|
};
|
|
176
177
|
cache.setSuccess(secret, auth, record.expiresAt ? record.expiresAt.getTime() : null);
|
|
177
178
|
return auth;
|
|
178
|
-
} catch {
|
|
179
|
+
} catch (err) {
|
|
180
|
+
if (isTransientDbError(err)) {
|
|
181
|
+
throw new AuthResolutionUnavailableError(err);
|
|
182
|
+
}
|
|
179
183
|
return null;
|
|
180
184
|
}
|
|
181
185
|
}
|
|
@@ -271,7 +275,16 @@ async function resolveAuthFromRequestDetailed(req) {
|
|
|
271
275
|
if (!apiKey) {
|
|
272
276
|
return { auth: null, status: resolveUnauthenticatedStatus() };
|
|
273
277
|
}
|
|
274
|
-
|
|
278
|
+
let apiAuth;
|
|
279
|
+
try {
|
|
280
|
+
apiAuth = await resolveApiKeyAuth(apiKey);
|
|
281
|
+
} catch (err) {
|
|
282
|
+
if (!(err instanceof AuthResolutionUnavailableError)) {
|
|
283
|
+
throw err;
|
|
284
|
+
}
|
|
285
|
+
hadUnavailableResolution = true;
|
|
286
|
+
return { auth: null, status: resolveUnauthenticatedStatus() };
|
|
287
|
+
}
|
|
275
288
|
if (!apiAuth) {
|
|
276
289
|
return { auth: null, status: resolveUnauthenticatedStatus() };
|
|
277
290
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/auth/server.ts"],
|
|
4
|
-
"sourcesContent": ["import { cookies } from 'next/headers.js'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { verifyJwt } from './jwt'\nimport { getSharedApiKeyAuthCache } from './apiKeyAuthCache'\n\nconst TENANT_COOKIE_NAME = 'om_selected_tenant'\nconst ORGANIZATION_COOKIE_NAME = 'om_selected_org'\nconst ALL_ORGANIZATIONS_COOKIE_VALUE = '__all__'\nconst SUPERADMIN_ROLE = 'superadmin'\n\nexport type AuthContext = {\n sub: string\n sid?: string | null\n tenantId: string | null\n orgId: string | null\n email?: string\n roles?: string[]\n isApiKey?: boolean\n userId?: string\n keyId?: string\n keyName?: string\n [k: string]: unknown\n} | null\n\ntype CookieOverride = { applied: boolean; value: string | null }\n// 'error' means auth could not be determined because of a transient/unexpected\n// failure (DB unavailable, connection-pool exhaustion, statement timeout, \u2026) \u2014\n// NOT because the token is genuinely invalid. Callers MUST NOT clear the user's\n// session cookies for 'error'; doing so would log everyone out at once during a\n// shared infrastructure blip (issue #4176).\ntype AuthResolutionStatus = 'authenticated' | 'missing' | 'invalid' | 'error'\ntype AuthResolution = {\n auth: AuthContext\n status: AuthResolutionStatus\n}\n\n// Thrown by `resolveCanonicalInteractiveAuthContext` when the canonical auth\n// lookup itself fails unexpectedly (as opposed to resolving to a genuine\n// \"not authorized\" null). Kept distinct so resolvers can map it to the\n// non-destructive 'error' status instead of the cookie-clearing 'invalid'.\nexport class AuthResolutionUnavailableError extends Error {\n constructor(cause?: unknown) {\n super('Auth resolution unavailable')\n this.name = 'AuthResolutionUnavailableError'\n if (cause !== undefined) (this as { cause?: unknown }).cause = cause\n }\n}\n\n// Symbol-keyed trusted auth context. Set on synthetic Request objects by\n// callers that have already authenticated (e.g. the AI in-process operation\n// runner) so downstream auth resolution short-circuits without re-running\n// cookie/JWT/API-key parsing. The hook is fail-open: if absent the normal\n// resolution path runs unchanged.\nexport const TRUSTED_AUTH_CONTEXT_SYMBOL = Symbol.for('open-mercato.auth.trustedContext')\n\nexport type TrustedAuthContextEnvelope = {\n auth: AuthContext\n status?: AuthResolutionStatus\n}\n\nexport function attachTrustedAuthContext(\n request: Request,\n envelope: TrustedAuthContextEnvelope\n): Request {\n ;(request as unknown as Record<symbol, TrustedAuthContextEnvelope>)[TRUSTED_AUTH_CONTEXT_SYMBOL] = envelope\n return request\n}\n\nfunction readTrustedAuthContext(request: Request): TrustedAuthContextEnvelope | null {\n const carrier = request as unknown as Record<symbol, unknown>\n const envelope = carrier[TRUSTED_AUTH_CONTEXT_SYMBOL]\n if (!envelope || typeof envelope !== 'object') return null\n const candidate = envelope as TrustedAuthContextEnvelope\n if (!('auth' in candidate)) return null\n return candidate\n}\n\nfunction decodeCookieValue(raw: string | undefined): string | null {\n if (raw === undefined) return null\n try {\n const decoded = decodeURIComponent(raw)\n return decoded ?? null\n } catch {\n return raw ?? null\n }\n}\n\nfunction readCookieFromHeader(header: string | null | undefined, name: string): string | undefined {\n if (!header) return undefined\n const parts = header.split(';')\n for (const part of parts) {\n const trimmed = part.trim()\n if (trimmed.startsWith(`${name}=`)) {\n return trimmed.slice(name.length + 1)\n }\n }\n return undefined\n}\n\nfunction resolveTenantOverride(raw: string | undefined): CookieOverride {\n if (raw === undefined) return { applied: false, value: null }\n const decoded = decodeCookieValue(raw)\n if (!decoded) return { applied: true, value: null }\n const trimmed = decoded.trim()\n if (!trimmed) return { applied: true, value: null }\n return { applied: true, value: trimmed }\n}\n\nfunction resolveOrganizationOverride(raw: string | undefined): CookieOverride {\n if (raw === undefined) return { applied: false, value: null }\n const decoded = decodeCookieValue(raw)\n if (!decoded || decoded === ALL_ORGANIZATIONS_COOKIE_VALUE) {\n return { applied: true, value: null }\n }\n const trimmed = decoded.trim()\n if (!trimmed || trimmed === ALL_ORGANIZATIONS_COOKIE_VALUE) {\n return { applied: true, value: null }\n }\n return { applied: true, value: trimmed }\n}\n\nfunction isSuperAdminAuth(auth: AuthContext | null | undefined): boolean {\n if (!auth) return false\n return (auth as Record<string, unknown>).isSuperAdmin === true\n}\n\nfunction applySuperAdminScope(\n auth: AuthContext,\n tenantCookie: string | undefined,\n orgCookie: string | undefined\n): AuthContext {\n if (!auth || !isSuperAdminAuth(auth)) return auth\n\n const tenantOverride = resolveTenantOverride(tenantCookie)\n const orgOverride = resolveOrganizationOverride(orgCookie)\n if (!tenantOverride.applied && !orgOverride.applied) return auth\n\n type MutableAuthContext = Exclude<AuthContext, null> & {\n actorTenantId?: string | null\n actorOrgId?: string | null\n }\n const baseAuth = auth as Exclude<AuthContext, null>\n const next: MutableAuthContext = { ...baseAuth }\n if (tenantOverride.applied) {\n if (!('actorTenantId' in next)) next.actorTenantId = auth?.tenantId ?? null\n next.tenantId = tenantOverride.value\n }\n if (orgOverride.applied) {\n if (!('actorOrgId' in next)) next.actorOrgId = auth?.orgId ?? null\n next.orgId = orgOverride.value\n }\n next.isSuperAdmin = true\n const existingRoles = Array.isArray(next.roles) ? next.roles : []\n if (!existingRoles.some((role) => typeof role === 'string' && role.trim().toLowerCase() === SUPERADMIN_ROLE)) {\n next.roles = [...existingRoles, 'superadmin']\n }\n return next\n}\n\nasync function resolveApiKeyAuth(secret: string): Promise<AuthContext> {\n if (!secret) return null\n const cache = getSharedApiKeyAuthCache()\n const cached = cache.get(secret)\n if (cached !== undefined) return cached as AuthContext\n try {\n const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n const { findApiKeyBySecret } = await import('@open-mercato/core/modules/api_keys/services/apiKeyService')\n const { Role, RoleAcl, User } = await import('@open-mercato/core/modules/auth/data/entities')\n const { Organization, Tenant } = await import('@open-mercato/core/modules/directory/data/entities')\n\n const record = await findApiKeyBySecret(em, secret)\n if (!record) {\n cache.setMiss(secret)\n return null\n }\n\n const roleIds = Array.isArray(record.rolesJson)\n ? record.rolesJson.filter((value): value is string => typeof value === 'string' && value.length > 0)\n : []\n const roles = roleIds.length\n ? await em.find(Role, { id: { $in: roleIds }, deletedAt: null })\n : []\n const roleNames = roles.map((role) => role.name).filter((name): name is string => typeof name === 'string' && name.length > 0)\n\n let keyIsSuperAdmin = false\n if (roleIds.length) {\n const superAcl = await em.findOne(\n RoleAcl,\n { role: { $in: roleIds } as any, isSuperAdmin: true, deletedAt: null } as any,\n )\n keyIsSuperAdmin = !!(superAcl && (superAcl as { isSuperAdmin?: boolean }).isSuperAdmin)\n }\n\n if (cache.shouldWriteLastUsed(record.id)) {\n try {\n record.lastUsedAt = new Date()\n await em.persist(record).flush()\n } catch {\n // best-effort update; ignore write failures\n }\n }\n\n // For session keys, use sessionUserId; for regular keys, use createdBy\n const actualUserId = record.sessionUserId ?? record.createdBy ?? null\n\n if (actualUserId) {\n const user = await em.findOne(User, { id: actualUserId, deletedAt: null })\n if (!user) {\n cache.setMiss(secret)\n return null\n }\n if ((user.tenantId ?? null) !== (record.tenantId ?? null)) {\n cache.setMiss(secret)\n return null\n }\n if ((user.organizationId ?? null) !== (record.organizationId ?? null)) {\n cache.setMiss(secret)\n return null\n }\n } else {\n if (record.tenantId) {\n const tenant = await em.findOne(Tenant, { id: record.tenantId, deletedAt: null, isActive: true })\n if (!tenant) {\n cache.setMiss(secret)\n return null\n }\n }\n if (record.organizationId) {\n const organization = await em.findOne(Organization, { id: record.organizationId, deletedAt: null, isActive: true })\n if (!organization) {\n cache.setMiss(secret)\n return null\n }\n if (record.tenantId && String(organization.tenant.id) !== record.tenantId) {\n cache.setMiss(secret)\n return null\n }\n }\n }\n\n const auth: Exclude<AuthContext, null> = {\n sub: `api_key:${record.id}`,\n tenantId: record.tenantId ?? null,\n orgId: record.organizationId ?? null,\n roles: roleNames,\n isApiKey: true,\n isSuperAdmin: keyIsSuperAdmin,\n keyId: record.id,\n keyName: record.name,\n ...(actualUserId ? { userId: actualUserId } : {}),\n }\n cache.setSuccess(secret, auth, record.expiresAt ? record.expiresAt.getTime() : null)\n return auth\n } catch {\n return null\n }\n}\n\nfunction extractApiKey(req: Request): string | null {\n const header = (req.headers.get('x-api-key') || '').trim()\n if (header) return header\n const authHeader = (req.headers.get('authorization') || '').trim()\n if (authHeader.toLowerCase().startsWith('apikey ')) {\n return authHeader.slice(7).trim()\n }\n return null\n}\n\nasync function resolveCanonicalInteractiveAuthContext(auth: AuthContext): Promise<AuthContext> {\n if (!auth || auth.isApiKey) return auth\n if (typeof auth.sub !== 'string' || auth.sub.trim().length === 0) return null\n\n try {\n const [{ createRequestContainer }, { resolveCanonicalStaffAuthContext }] = await Promise.all([\n import('@open-mercato/shared/lib/di/container'),\n import('@open-mercato/core/modules/auth/lib/sessionIntegrity'),\n ])\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n return await resolveCanonicalStaffAuthContext(em, auth)\n } catch (err) {\n // A thrown error here means we could not check the token against the\n // database (DB down, pool exhausted, timeout). This is NOT the same as the\n // token being invalid \u2014 surface it so callers keep the session intact and\n // fail the request transiently instead of force-logging the user out.\n throw new AuthResolutionUnavailableError(err)\n }\n}\n\nexport async function resolveAuthFromCookiesDetailed(): Promise<AuthResolution> {\n const cookieStore = await cookies()\n const token = cookieStore.get('auth_token')?.value\n if (!token) return { auth: null, status: 'missing' }\n try {\n const payload = verifyJwt(token) as AuthContext\n if (!payload) return { auth: null, status: 'invalid' }\n if (payload.type === 'customer') return { auth: null, status: 'invalid' }\n const canonicalAuth = await resolveCanonicalInteractiveAuthContext(payload)\n if (!canonicalAuth) return { auth: null, status: 'invalid' }\n const tenantCookie = cookieStore.get(TENANT_COOKIE_NAME)?.value\n const orgCookie = cookieStore.get(ORGANIZATION_COOKIE_NAME)?.value\n return {\n auth: applySuperAdminScope(canonicalAuth, tenantCookie, orgCookie),\n status: 'authenticated',\n }\n } catch (err) {\n if (err instanceof AuthResolutionUnavailableError) return { auth: null, status: 'error' }\n return { auth: null, status: 'invalid' }\n }\n}\n\nexport async function getAuthFromCookies(): Promise<AuthContext> {\n return (await resolveAuthFromCookiesDetailed()).auth\n}\n\nexport async function resolveAuthFromRequestDetailed(req: Request): Promise<AuthResolution> {\n const trusted = readTrustedAuthContext(req)\n if (trusted) {\n return {\n auth: trusted.auth,\n status: trusted.status ?? (trusted.auth ? 'authenticated' : 'missing'),\n }\n }\n const cookieHeader = req.headers.get('cookie') || ''\n const tenantCookie = readCookieFromHeader(cookieHeader, TENANT_COOKIE_NAME)\n const orgCookie = readCookieFromHeader(cookieHeader, ORGANIZATION_COOKIE_NAME)\n const authHeader = (req.headers.get('authorization') || '').trim()\n let token: string | undefined\n let hadInvalidInteractiveToken = false\n let hadUnavailableResolution = false\n if (authHeader.toLowerCase().startsWith('bearer ')) token = authHeader.slice(7).trim()\n if (!token) {\n const match = cookieHeader.match(/(?:^|;\\s*)auth_token=([^;]+)/)\n if (match) token = decodeURIComponent(match[1])\n }\n if (token) {\n try {\n const payload = verifyJwt(token) as AuthContext\n if (payload && payload.type === 'customer') return { auth: null, status: 'invalid' }\n if (payload) {\n const canonicalAuth = await resolveCanonicalInteractiveAuthContext(payload)\n if (canonicalAuth) {\n return {\n auth: applySuperAdminScope(canonicalAuth, tenantCookie, orgCookie),\n status: 'authenticated',\n }\n }\n hadInvalidInteractiveToken = true\n }\n } catch (err) {\n // Transient/unexpected canonical-resolution failures must not invalidate\n // the token \u2014 surface them so we return 'error' (retryable) rather than\n // clearing the session cookies.\n if (err instanceof AuthResolutionUnavailableError) hadUnavailableResolution = true\n else hadInvalidInteractiveToken = true\n }\n }\n\n const resolveUnauthenticatedStatus = (): AuthResolutionStatus =>\n hadUnavailableResolution ? 'error' : hadInvalidInteractiveToken ? 'invalid' : 'missing'\n\n const apiKey = extractApiKey(req)\n if (!apiKey) {\n return { auth: null, status: resolveUnauthenticatedStatus() }\n }\n const apiAuth = await resolveApiKeyAuth(apiKey)\n if (!apiAuth) {\n return { auth: null, status: resolveUnauthenticatedStatus() }\n }\n return {\n auth: applySuperAdminScope(apiAuth, tenantCookie, orgCookie),\n status: 'authenticated',\n }\n}\n\nexport async function getAuthFromRequest(req: Request): Promise<AuthContext> {\n return (await resolveAuthFromRequestDetailed(req)).auth\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,eAAe;AAExB,SAAS,iBAAiB;AAC1B,SAAS,gCAAgC;
|
|
4
|
+
"sourcesContent": ["import { cookies } from 'next/headers.js'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { verifyJwt } from './jwt'\nimport { getSharedApiKeyAuthCache } from './apiKeyAuthCache'\nimport { isTransientDbError } from '@open-mercato/shared/lib/db/pg-errors'\n\nconst TENANT_COOKIE_NAME = 'om_selected_tenant'\nconst ORGANIZATION_COOKIE_NAME = 'om_selected_org'\nconst ALL_ORGANIZATIONS_COOKIE_VALUE = '__all__'\nconst SUPERADMIN_ROLE = 'superadmin'\n\nexport type AuthContext = {\n sub: string\n sid?: string | null\n tenantId: string | null\n orgId: string | null\n email?: string\n roles?: string[]\n isApiKey?: boolean\n userId?: string\n keyId?: string\n keyName?: string\n [k: string]: unknown\n} | null\n\ntype CookieOverride = { applied: boolean; value: string | null }\n// 'error' means auth could not be determined because of a transient/unexpected\n// failure (DB unavailable, connection-pool exhaustion, statement timeout, \u2026) \u2014\n// NOT because the token is genuinely invalid. Callers MUST NOT clear the user's\n// session cookies for 'error'; doing so would log everyone out at once during a\n// shared infrastructure blip (issue #4176).\ntype AuthResolutionStatus = 'authenticated' | 'missing' | 'invalid' | 'error'\ntype AuthResolution = {\n auth: AuthContext\n status: AuthResolutionStatus\n}\n\n// Thrown by `resolveCanonicalInteractiveAuthContext` when the canonical auth\n// lookup itself fails unexpectedly (as opposed to resolving to a genuine\n// \"not authorized\" null). Kept distinct so resolvers can map it to the\n// non-destructive 'error' status instead of the cookie-clearing 'invalid'.\nexport class AuthResolutionUnavailableError extends Error {\n constructor(cause?: unknown) {\n super('Auth resolution unavailable')\n this.name = 'AuthResolutionUnavailableError'\n if (cause !== undefined) (this as { cause?: unknown }).cause = cause\n }\n}\n\n// Symbol-keyed trusted auth context. Set on synthetic Request objects by\n// callers that have already authenticated (e.g. the AI in-process operation\n// runner) so downstream auth resolution short-circuits without re-running\n// cookie/JWT/API-key parsing. The hook is fail-open: if absent the normal\n// resolution path runs unchanged.\nexport const TRUSTED_AUTH_CONTEXT_SYMBOL = Symbol.for('open-mercato.auth.trustedContext')\n\nexport type TrustedAuthContextEnvelope = {\n auth: AuthContext\n status?: AuthResolutionStatus\n}\n\nexport function attachTrustedAuthContext(\n request: Request,\n envelope: TrustedAuthContextEnvelope\n): Request {\n ;(request as unknown as Record<symbol, TrustedAuthContextEnvelope>)[TRUSTED_AUTH_CONTEXT_SYMBOL] = envelope\n return request\n}\n\nfunction readTrustedAuthContext(request: Request): TrustedAuthContextEnvelope | null {\n const carrier = request as unknown as Record<symbol, unknown>\n const envelope = carrier[TRUSTED_AUTH_CONTEXT_SYMBOL]\n if (!envelope || typeof envelope !== 'object') return null\n const candidate = envelope as TrustedAuthContextEnvelope\n if (!('auth' in candidate)) return null\n return candidate\n}\n\nfunction decodeCookieValue(raw: string | undefined): string | null {\n if (raw === undefined) return null\n try {\n const decoded = decodeURIComponent(raw)\n return decoded ?? null\n } catch {\n return raw ?? null\n }\n}\n\nfunction readCookieFromHeader(header: string | null | undefined, name: string): string | undefined {\n if (!header) return undefined\n const parts = header.split(';')\n for (const part of parts) {\n const trimmed = part.trim()\n if (trimmed.startsWith(`${name}=`)) {\n return trimmed.slice(name.length + 1)\n }\n }\n return undefined\n}\n\nfunction resolveTenantOverride(raw: string | undefined): CookieOverride {\n if (raw === undefined) return { applied: false, value: null }\n const decoded = decodeCookieValue(raw)\n if (!decoded) return { applied: true, value: null }\n const trimmed = decoded.trim()\n if (!trimmed) return { applied: true, value: null }\n return { applied: true, value: trimmed }\n}\n\nfunction resolveOrganizationOverride(raw: string | undefined): CookieOverride {\n if (raw === undefined) return { applied: false, value: null }\n const decoded = decodeCookieValue(raw)\n if (!decoded || decoded === ALL_ORGANIZATIONS_COOKIE_VALUE) {\n return { applied: true, value: null }\n }\n const trimmed = decoded.trim()\n if (!trimmed || trimmed === ALL_ORGANIZATIONS_COOKIE_VALUE) {\n return { applied: true, value: null }\n }\n return { applied: true, value: trimmed }\n}\n\nfunction isSuperAdminAuth(auth: AuthContext | null | undefined): boolean {\n if (!auth) return false\n return (auth as Record<string, unknown>).isSuperAdmin === true\n}\n\nfunction applySuperAdminScope(\n auth: AuthContext,\n tenantCookie: string | undefined,\n orgCookie: string | undefined\n): AuthContext {\n if (!auth || !isSuperAdminAuth(auth)) return auth\n\n const tenantOverride = resolveTenantOverride(tenantCookie)\n const orgOverride = resolveOrganizationOverride(orgCookie)\n if (!tenantOverride.applied && !orgOverride.applied) return auth\n\n type MutableAuthContext = Exclude<AuthContext, null> & {\n actorTenantId?: string | null\n actorOrgId?: string | null\n }\n const baseAuth = auth as Exclude<AuthContext, null>\n const next: MutableAuthContext = { ...baseAuth }\n if (tenantOverride.applied) {\n if (!('actorTenantId' in next)) next.actorTenantId = auth?.tenantId ?? null\n next.tenantId = tenantOverride.value\n }\n if (orgOverride.applied) {\n if (!('actorOrgId' in next)) next.actorOrgId = auth?.orgId ?? null\n next.orgId = orgOverride.value\n }\n next.isSuperAdmin = true\n const existingRoles = Array.isArray(next.roles) ? next.roles : []\n if (!existingRoles.some((role) => typeof role === 'string' && role.trim().toLowerCase() === SUPERADMIN_ROLE)) {\n next.roles = [...existingRoles, 'superadmin']\n }\n return next\n}\n\nasync function resolveApiKeyAuth(secret: string): Promise<AuthContext> {\n if (!secret) return null\n const cache = getSharedApiKeyAuthCache()\n const cached = cache.get(secret)\n if (cached !== undefined) return cached as AuthContext\n try {\n const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n const { findApiKeyBySecret } = await import('@open-mercato/core/modules/api_keys/services/apiKeyService')\n const { Role, RoleAcl, User } = await import('@open-mercato/core/modules/auth/data/entities')\n const { Organization, Tenant } = await import('@open-mercato/core/modules/directory/data/entities')\n\n const record = await findApiKeyBySecret(em, secret)\n if (!record) {\n cache.setMiss(secret)\n return null\n }\n\n const roleIds = Array.isArray(record.rolesJson)\n ? record.rolesJson.filter((value): value is string => typeof value === 'string' && value.length > 0)\n : []\n const roles = roleIds.length\n ? await em.find(Role, { id: { $in: roleIds }, deletedAt: null })\n : []\n const roleNames = roles.map((role) => role.name).filter((name): name is string => typeof name === 'string' && name.length > 0)\n\n let keyIsSuperAdmin = false\n if (roleIds.length) {\n const superAcl = await em.findOne(\n RoleAcl,\n { role: { $in: roleIds } as any, isSuperAdmin: true, deletedAt: null } as any,\n )\n keyIsSuperAdmin = !!(superAcl && (superAcl as { isSuperAdmin?: boolean }).isSuperAdmin)\n }\n\n if (cache.shouldWriteLastUsed(record.id)) {\n try {\n record.lastUsedAt = new Date()\n await em.persist(record).flush()\n } catch {\n // best-effort update; ignore write failures\n }\n }\n\n // For session keys, use sessionUserId; for regular keys, use createdBy\n const actualUserId = record.sessionUserId ?? record.createdBy ?? null\n\n if (actualUserId) {\n const user = await em.findOne(User, { id: actualUserId, deletedAt: null })\n if (!user) {\n cache.setMiss(secret)\n return null\n }\n if ((user.tenantId ?? null) !== (record.tenantId ?? null)) {\n cache.setMiss(secret)\n return null\n }\n if ((user.organizationId ?? null) !== (record.organizationId ?? null)) {\n cache.setMiss(secret)\n return null\n }\n } else {\n if (record.tenantId) {\n const tenant = await em.findOne(Tenant, { id: record.tenantId, deletedAt: null, isActive: true })\n if (!tenant) {\n cache.setMiss(secret)\n return null\n }\n }\n if (record.organizationId) {\n const organization = await em.findOne(Organization, { id: record.organizationId, deletedAt: null, isActive: true })\n if (!organization) {\n cache.setMiss(secret)\n return null\n }\n if (record.tenantId && String(organization.tenant.id) !== record.tenantId) {\n cache.setMiss(secret)\n return null\n }\n }\n }\n\n const auth: Exclude<AuthContext, null> = {\n sub: `api_key:${record.id}`,\n tenantId: record.tenantId ?? null,\n orgId: record.organizationId ?? null,\n roles: roleNames,\n isApiKey: true,\n isSuperAdmin: keyIsSuperAdmin,\n keyId: record.id,\n keyName: record.name,\n ...(actualUserId ? { userId: actualUserId } : {}),\n }\n cache.setSuccess(secret, auth, record.expiresAt ? record.expiresAt.getTime() : null)\n return auth\n } catch (err) {\n // A transient DB failure (pool exhausted, `max_connections` reached, DB\n // restarting) means we could not confirm the key \u2014 NOT that it is invalid.\n // Surface it so callers return a retryable 503 instead of masking it as an\n // auth miss (401). Genuine misses already returned `null` above.\n if (isTransientDbError(err)) {\n throw new AuthResolutionUnavailableError(err)\n }\n return null\n }\n}\n\nfunction extractApiKey(req: Request): string | null {\n const header = (req.headers.get('x-api-key') || '').trim()\n if (header) return header\n const authHeader = (req.headers.get('authorization') || '').trim()\n if (authHeader.toLowerCase().startsWith('apikey ')) {\n return authHeader.slice(7).trim()\n }\n return null\n}\n\nasync function resolveCanonicalInteractiveAuthContext(auth: AuthContext): Promise<AuthContext> {\n if (!auth || auth.isApiKey) return auth\n if (typeof auth.sub !== 'string' || auth.sub.trim().length === 0) return null\n\n try {\n const [{ createRequestContainer }, { resolveCanonicalStaffAuthContext }] = await Promise.all([\n import('@open-mercato/shared/lib/di/container'),\n import('@open-mercato/core/modules/auth/lib/sessionIntegrity'),\n ])\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n return await resolveCanonicalStaffAuthContext(em, auth)\n } catch (err) {\n // A thrown error here means we could not check the token against the\n // database (DB down, pool exhausted, timeout). This is NOT the same as the\n // token being invalid \u2014 surface it so callers keep the session intact and\n // fail the request transiently instead of force-logging the user out.\n throw new AuthResolutionUnavailableError(err)\n }\n}\n\nexport async function resolveAuthFromCookiesDetailed(): Promise<AuthResolution> {\n const cookieStore = await cookies()\n const token = cookieStore.get('auth_token')?.value\n if (!token) return { auth: null, status: 'missing' }\n try {\n const payload = verifyJwt(token) as AuthContext\n if (!payload) return { auth: null, status: 'invalid' }\n if (payload.type === 'customer') return { auth: null, status: 'invalid' }\n const canonicalAuth = await resolveCanonicalInteractiveAuthContext(payload)\n if (!canonicalAuth) return { auth: null, status: 'invalid' }\n const tenantCookie = cookieStore.get(TENANT_COOKIE_NAME)?.value\n const orgCookie = cookieStore.get(ORGANIZATION_COOKIE_NAME)?.value\n return {\n auth: applySuperAdminScope(canonicalAuth, tenantCookie, orgCookie),\n status: 'authenticated',\n }\n } catch (err) {\n if (err instanceof AuthResolutionUnavailableError) return { auth: null, status: 'error' }\n return { auth: null, status: 'invalid' }\n }\n}\n\nexport async function getAuthFromCookies(): Promise<AuthContext> {\n return (await resolveAuthFromCookiesDetailed()).auth\n}\n\nexport async function resolveAuthFromRequestDetailed(req: Request): Promise<AuthResolution> {\n const trusted = readTrustedAuthContext(req)\n if (trusted) {\n return {\n auth: trusted.auth,\n status: trusted.status ?? (trusted.auth ? 'authenticated' : 'missing'),\n }\n }\n const cookieHeader = req.headers.get('cookie') || ''\n const tenantCookie = readCookieFromHeader(cookieHeader, TENANT_COOKIE_NAME)\n const orgCookie = readCookieFromHeader(cookieHeader, ORGANIZATION_COOKIE_NAME)\n const authHeader = (req.headers.get('authorization') || '').trim()\n let token: string | undefined\n let hadInvalidInteractiveToken = false\n let hadUnavailableResolution = false\n if (authHeader.toLowerCase().startsWith('bearer ')) token = authHeader.slice(7).trim()\n if (!token) {\n const match = cookieHeader.match(/(?:^|;\\s*)auth_token=([^;]+)/)\n if (match) token = decodeURIComponent(match[1])\n }\n if (token) {\n try {\n const payload = verifyJwt(token) as AuthContext\n if (payload && payload.type === 'customer') return { auth: null, status: 'invalid' }\n if (payload) {\n const canonicalAuth = await resolveCanonicalInteractiveAuthContext(payload)\n if (canonicalAuth) {\n return {\n auth: applySuperAdminScope(canonicalAuth, tenantCookie, orgCookie),\n status: 'authenticated',\n }\n }\n hadInvalidInteractiveToken = true\n }\n } catch (err) {\n // Transient/unexpected canonical-resolution failures must not invalidate\n // the token \u2014 surface them so we return 'error' (retryable) rather than\n // clearing the session cookies.\n if (err instanceof AuthResolutionUnavailableError) hadUnavailableResolution = true\n else hadInvalidInteractiveToken = true\n }\n }\n\n const resolveUnauthenticatedStatus = (): AuthResolutionStatus =>\n hadUnavailableResolution ? 'error' : hadInvalidInteractiveToken ? 'invalid' : 'missing'\n\n const apiKey = extractApiKey(req)\n if (!apiKey) {\n return { auth: null, status: resolveUnauthenticatedStatus() }\n }\n let apiAuth: AuthContext\n try {\n apiAuth = await resolveApiKeyAuth(apiKey)\n } catch (err) {\n // Only a transient canonical-resolution failure maps to 'error' (retryable\n // 503), mirroring the interactive-token path above. Anything else is an\n // unexpected bug \u2014 rethrow so it surfaces as a 500 rather than being masked\n // as an auth failure.\n if (!(err instanceof AuthResolutionUnavailableError)) {\n throw err\n }\n hadUnavailableResolution = true\n return { auth: null, status: resolveUnauthenticatedStatus() }\n }\n if (!apiAuth) {\n return { auth: null, status: resolveUnauthenticatedStatus() }\n }\n return {\n auth: applySuperAdminScope(apiAuth, tenantCookie, orgCookie),\n status: 'authenticated',\n }\n}\n\nexport async function getAuthFromRequest(req: Request): Promise<AuthContext> {\n return (await resolveAuthFromRequestDetailed(req)).auth\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const CLIENT_ONLY_STUB_NAMESPACE = "om-client-only-stub";
|
|
2
|
+
const LOCAL_IMPORT_PATTERN = /^(\.{1,2}\/|@\/)/;
|
|
3
|
+
const CLIENT_ONLY_SUFFIX_PATTERN = /(^|[\\/])[^\\/]+\.client(\.[cm]?[jt]sx?)?$/;
|
|
4
|
+
function isClientOnlyModulePath(importPath) {
|
|
5
|
+
if (!LOCAL_IMPORT_PATTERN.test(importPath)) return false;
|
|
6
|
+
return CLIENT_ONLY_SUFFIX_PATTERN.test(importPath);
|
|
7
|
+
}
|
|
8
|
+
function encodeJsStringLiteral(value) {
|
|
9
|
+
let encoded = "";
|
|
10
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
11
|
+
const char = value[index];
|
|
12
|
+
if (char === '"' || char === "\\") {
|
|
13
|
+
encoded += `\\${char}`;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
const code = value.charCodeAt(index);
|
|
17
|
+
if (code < 32 || code > 126) {
|
|
18
|
+
encoded += `\\u${code.toString(16).padStart(4, "0")}`;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
encoded += char;
|
|
22
|
+
}
|
|
23
|
+
return `"${encoded}"`;
|
|
24
|
+
}
|
|
25
|
+
function renderClientOnlyModuleStub(importPath) {
|
|
26
|
+
const message = `[internal] Client-only module ${importPath} is not available in the CLI runtime. It is excluded from the CLI bundle because it renders browser components.`;
|
|
27
|
+
return [
|
|
28
|
+
`function clientOnlyModuleUnavailable() { throw new Error(${encodeJsStringLiteral(message)}) }`,
|
|
29
|
+
"export default clientOnlyModuleUnavailable"
|
|
30
|
+
].join("\n");
|
|
31
|
+
}
|
|
32
|
+
function createClientOnlyStubPlugin() {
|
|
33
|
+
return {
|
|
34
|
+
name: "client-only-stub",
|
|
35
|
+
setup(build) {
|
|
36
|
+
build.onResolve({ filter: CLIENT_ONLY_SUFFIX_PATTERN }, (args) => {
|
|
37
|
+
if (args.kind !== "dynamic-import") return null;
|
|
38
|
+
if (!isClientOnlyModulePath(args.path)) return null;
|
|
39
|
+
return { path: args.path, namespace: CLIENT_ONLY_STUB_NAMESPACE };
|
|
40
|
+
});
|
|
41
|
+
build.onLoad({ filter: /.*/, namespace: CLIENT_ONLY_STUB_NAMESPACE }, (args) => ({
|
|
42
|
+
contents: renderClientOnlyModuleStub(args.path),
|
|
43
|
+
loader: "js"
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export {
|
|
49
|
+
CLIENT_ONLY_STUB_NAMESPACE,
|
|
50
|
+
createClientOnlyStubPlugin,
|
|
51
|
+
encodeJsStringLiteral,
|
|
52
|
+
isClientOnlyModulePath,
|
|
53
|
+
renderClientOnlyModuleStub
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=clientOnlyModules.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/bootstrap/clientOnlyModules.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Client-only modules (`*.client.tsx`) hold browser components and must never enter the\n * CLI bundle graph. App modules are bundled from source by `compileAndImport`, and esbuild\n * inlines dynamic imports into the single output file, hoisting the client file's static\n * imports to the top of the bundle. A wrapper such as `@open-mercato/ui/backend/charts`\n * would then execute on every CLI start and fail on bare Next.js specifiers that Node's\n * ESM resolver cannot resolve.\n *\n * Resolving these files to an inert stub keeps the owning `widget.ts` importable (the CLI\n * reads its metadata when seeding dashboards) while cutting the browser-only subgraph.\n *\n * Only `import()` expressions are stubbed. That is the documented way a server-side\n * `widget.ts` reaches its browser component (`lazyDashboardWidget(() => import('./widget.client'))`),\n * and its result is consumed as a namespace object, so a default-only stub is sufficient.\n * Static imports are left to the bundler: they may request named bindings the stub cannot\n * provide, which esbuild rejects with `No matching export`, failing the whole CLI bundle \u2014\n * the exact class of breakage this module exists to prevent. Restricting the rewrite to\n * dynamic imports also keeps server-side helpers that merely follow a `*.client.ts` naming\n * convention working untouched.\n */\n\nexport const CLIENT_ONLY_STUB_NAMESPACE = 'om-client-only-stub'\n\nconst LOCAL_IMPORT_PATTERN = /^(\\.{1,2}\\/|@\\/)/\nconst CLIENT_ONLY_SUFFIX_PATTERN = /(^|[\\\\/])[^\\\\/]+\\.client(\\.[cm]?[jt]sx?)?$/\n\n/**\n * Local (relative or `@/` aliased) imports of a `*.client` module. Bare package specifiers\n * are left alone because the bundler already marks them external, so they never get inlined.\n */\nexport function isClientOnlyModulePath(importPath: string): boolean {\n if (!LOCAL_IMPORT_PATTERN.test(importPath)) return false\n return CLIENT_ONLY_SUFFIX_PATTERN.test(importPath)\n}\n\n/**\n * Render a value as a JavaScript string literal safe to embed in generated source.\n * Everything outside printable ASCII is escaped to a `\\uXXXX` sequence, so no character\n * of the input can terminate the literal or be re-interpreted as code \u2014 `JSON.stringify`\n * alone is a JSON encoder, not a JavaScript-source escaper.\n */\nexport function encodeJsStringLiteral(value: string): string {\n let encoded = ''\n for (let index = 0; index < value.length; index += 1) {\n const char = value[index]\n if (char === '\"' || char === '\\\\') {\n encoded += `\\\\${char}`\n continue\n }\n const code = value.charCodeAt(index)\n if (code < 0x20 || code > 0x7e) {\n encoded += `\\\\u${code.toString(16).padStart(4, '0')}`\n continue\n }\n encoded += char\n }\n return `\"${encoded}\"`\n}\n\nexport function renderClientOnlyModuleStub(importPath: string): string {\n const message =\n `[internal] Client-only module ${importPath} is not available in the CLI runtime. ` +\n 'It is excluded from the CLI bundle because it renders browser components.'\n return [\n `function clientOnlyModuleUnavailable() { throw new Error(${encodeJsStringLiteral(message)}) }`,\n 'export default clientOnlyModuleUnavailable',\n ].join('\\n')\n}\n\nexport function createClientOnlyStubPlugin(): import('esbuild').Plugin {\n return {\n name: 'client-only-stub',\n setup(build) {\n build.onResolve({ filter: CLIENT_ONLY_SUFFIX_PATTERN }, (args) => {\n if (args.kind !== 'dynamic-import') return null\n if (!isClientOnlyModulePath(args.path)) return null\n return { path: args.path, namespace: CLIENT_ONLY_STUB_NAMESPACE }\n })\n build.onLoad({ filter: /.*/, namespace: CLIENT_ONLY_STUB_NAMESPACE }, (args) => ({\n contents: renderClientOnlyModuleStub(args.path),\n loader: 'js',\n }))\n },\n }\n}\n"],
|
|
5
|
+
"mappings": "AAqBO,MAAM,6BAA6B;AAE1C,MAAM,uBAAuB;AAC7B,MAAM,6BAA6B;AAM5B,SAAS,uBAAuB,YAA6B;AAClE,MAAI,CAAC,qBAAqB,KAAK,UAAU,EAAG,QAAO;AACnD,SAAO,2BAA2B,KAAK,UAAU;AACnD;AAQO,SAAS,sBAAsB,OAAuB;AAC3D,MAAI,UAAU;AACd,WAAS,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS,GAAG;AACpD,UAAM,OAAO,MAAM,KAAK;AACxB,QAAI,SAAS,OAAO,SAAS,MAAM;AACjC,iBAAW,KAAK,IAAI;AACpB;AAAA,IACF;AACA,UAAM,OAAO,MAAM,WAAW,KAAK;AACnC,QAAI,OAAO,MAAQ,OAAO,KAAM;AAC9B,iBAAW,MAAM,KAAK,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC;AACnD;AAAA,IACF;AACA,eAAW;AAAA,EACb;AACA,SAAO,IAAI,OAAO;AACpB;AAEO,SAAS,2BAA2B,YAA4B;AACrE,QAAM,UACJ,iCAAiC,UAAU;AAE7C,SAAO;AAAA,IACL,4DAA4D,sBAAsB,OAAO,CAAC;AAAA,IAC1F;AAAA,EACF,EAAE,KAAK,IAAI;AACb;AAEO,SAAS,6BAAuD;AACrE,SAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM,OAAO;AACX,YAAM,UAAU,EAAE,QAAQ,2BAA2B,GAAG,CAAC,SAAS;AAChE,YAAI,KAAK,SAAS,iBAAkB,QAAO;AAC3C,YAAI,CAAC,uBAAuB,KAAK,IAAI,EAAG,QAAO;AAC/C,eAAO,EAAE,MAAM,KAAK,MAAM,WAAW,2BAA2B;AAAA,MAClE,CAAC;AACD,YAAM,OAAO,EAAE,QAAQ,MAAM,WAAW,2BAA2B,GAAG,CAAC,UAAU;AAAA,QAC/E,UAAU,2BAA2B,KAAK,IAAI;AAAA,QAC9C,QAAQ;AAAA,MACV,EAAE;AAAA,IACJ;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|