@snokam/navbar 2.1.4 → 3.0.1

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/CHANGELOG.md CHANGED
@@ -3,6 +3,652 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [3.0.1](https://github.com/snokam/monorepo/compare/@snokam/navbar@3.0.0...@snokam/navbar@3.0.1) (2026-09-22)
7
+
8
+ **Note:** Version bump only for package @snokam/navbar
9
+
10
+ # [3.0.0](https://github.com/snokam/monorepo/compare/@snokam/navbar@2.1.11...@snokam/navbar@3.0.0) (2026-09-22)
11
+
12
+ - refactor!: one owner for tenancy and auth, short entry files (#2456) ([d8ae9cf](https://github.com/snokam/monorepo/commit/d8ae9cf23665d0e140b19c25f5b6a8a060a34553)), closes [#2456](https://github.com/snokam/monorepo/issues/2456) [#2457](https://github.com/snokam/monorepo/issues/2457)
13
+
14
+ ### BREAKING CHANGES
15
+
16
+ - Snokam.Security is replaced by Snokam.Runtime, Snokam.Tenancy
17
+ and Snokam.Auth. Consumers must update their ProjectReference and using
18
+ directives.
19
+
20
+ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
21
+
22
+ - refactor(api): resolve every endpoint gate in one place
23
+
24
+ Three independent reflection passes decided what an endpoint's authorization
25
+ gate was, each with slightly different rules:
26
+
27
+ - FunctionAuthorizationMiddleware.Resolve/ResolveScope/ResolveBusiness, three
28
+ caches keyed on the same entry point
29
+ - EndpointGateValidator.Validate, which treated any AuthorizeAttribute as gated
30
+ - SecuritySchemeDocumentFilter.Classify, which additionally split out the
31
+ PlatformService policy
32
+
33
+ Changing a gate rule meant finding and editing all three, and they could drift
34
+ apart silently: the OpenAPI document could describe an endpoint as anonymous
35
+ while the middleware enforced it, or the reverse.
36
+
37
+ There is now one EndpointGate record and one EndpointGates.Describe that
38
+ produces it. The record carries the superset of what the three callers needed —
39
+ kind, scope attribute, business, authorize data, whether it is an HTTP trigger,
40
+ and whether AllowAnonymous contradicts a gate — and each caller keeps its own
41
+ predicate over those fields rather than its own reflection.
42
+
43
+ EndpointGateValidator becomes GateStartupCheck, which now reads Kind and
44
+ Contradictory instead of re-deriving them. The OpenAPI filter reads Kind. The
45
+ middleware reads the rest, and loses its three static caches in favour of the
46
+ one inside EndpointGates.
47
+
48
+ The existing gate tests were repointed at the shared API rather than rewritten,
49
+ so they now assert the single implementation produces what all three passes did:
50
+ 201 tests pass, and grep confirms FunctionAuthorizeAttribute is reflected over
51
+ in exactly one file.
52
+
53
+ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
54
+
55
+ - refactor(api): make the entry file short and put each type where it belongs
56
+
57
+ ConfigureApi.cs was 151 lines and the first file anyone reads. It declared the
58
+ middleware order, roughly 25 DI registrations spanning six unrelated concerns,
59
+ OpenTelemetry, forwarded headers, the OpenAPI document and logging — so the
60
+ answer to "what is this API made of" was buried in the middle of a lambda.
61
+
62
+ It is now 32 lines that name the six things and delegate:
63
+
64
+ hostBuilder.ConfigureFunctionsWebApplication(ApiPipeline.Build);
65
+ hostBuilder.ConfigureServices(services => services
66
+ .AddApiCore(config).AddApiTelemetry().AddApiAuth(config)
67
+ .AddApiTenancy(config).AddApiNotifications().AddApiOpenApi(config));
68
+ hostBuilder.ConfigureLogging(ApiLogging.Configure);
69
+
70
+ Each of those lives in its own file under Startup/Services, the middleware order
71
+ in ApiPipeline, and the OpenAPI wiring in Startup/OpenApi alongside the document
72
+ filter, the tenant-aware options and ImplicitAuthFlow.
73
+
74
+ Several types were also filed under folders that did not describe them:
75
+
76
+ - ApiTokenVerifier sat in Tenancy/ but is API-key authentication; it now sits in
77
+ Authorization/Schemes next to the middleware that calls it.
78
+ - BindingDataHeaders sat in Tenancy/ but is generic header parsing, used by
79
+ Startup too; it moves to Http/.
80
+ - ForBusinessAttribute and RequiresCapabilityAttribute sat in Authorization/ but
81
+ their only consumer is the cron middleware; all three move to Scheduling/.
82
+ - The two auth schemes, CallerAuthentication and the three grant sources were
83
+ loose in Authorization/; they group under Schemes/ and Grants/.
84
+
85
+ Extracting Http/HostHeader.cs also removed a duplicate: the forwarded-host
86
+ header list and its normaliser existed in both BindingDataHeaders and
87
+ TenantMiddleware. There is now one of each.
88
+
89
+ Snokam.sln builds with 0 errors; 201 tests pass.
90
+
91
+ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
92
+
93
+ - refactor(frontend)!: rename @snokam/config to @snokam/tenant and give it the tenant logic
94
+
95
+ "config" said nothing about what the package did, and what it did was resolve
96
+ tenants. Inside, resolve-domain.ts was a 13-line pure re-export shim that every
97
+ other file imported through, so the actual entry point taught you nothing; and
98
+ server.ts (an env-var scraper) sat next to a "./server" export that pointed at a
99
+ different file entirely.
100
+
101
+ The package is now @snokam/tenant, and its folders say what they hold:
102
+
103
+ index.ts the client surface
104
+ server.ts the RSC surface
105
+ host.ts the one host normaliser
106
+ platform.ts the one place platform env vars are read
107
+ resolve/{client,cache,by-host}.ts
108
+ urls/{api-base,base-domain}.ts
109
+ runtime/{provider,env,transforms,local-urls}.ts
110
+ server/{request-host,token,urls}.ts
111
+
112
+ resolve-domain.ts is deleted. Three export subpaths become ".", "./server" and
113
+ "./middleware"; "./tenant" is gone, its host predicates having moved to the root
114
+ and resolveTenantApiBase having become backendUrl on "./server".
115
+
116
+ That package now owns four things that were previously copied around:
117
+
118
+ - Backend API base URL. resolveServerApiBase (auth), resolveTenantApiBase
119
+ (config), resolveEmployeesBackend (auth) and resolveServerAppUrl (auth) were
120
+ four implementations of the same env-key-then-domain-match-then-host-fallback
121
+ ladder, and apps imported two of them in the same file. There is now
122
+ backendUrl() and appUrl(); the old auth names survive as one-line re-exports
123
+ so the 153 call sites did not have to move. app-router/server.ts goes 93 lines
124
+ to 39.
125
+ - Host normalisation. Five copies of the localhost -> NEXT_PUBLIC_TENANT_HOST
126
+ fallback and eight of the forwarded-host read collapse into host.ts and
127
+ server/request-host.ts.
128
+ - Base-domain derivation. Four copies collapse into urls/base-domain.ts, which
129
+ names its three entries fromWindow, fromHost and fromTenantInfo.
130
+ - Platform env reads, which previously differed in whether they stripped a
131
+ trailing slash.
132
+
133
+ @snokam/auth also loses its duplicate AES-256-GCM ticket module: handoff and
134
+ impersonation were near-identical 79- and 97-line files. They now share
135
+ ticket/seal.ts and keep only what actually differs between them, which is the
136
+ TTL and whether a rejection is logged.
137
+
138
+ @snokam/tenant typechecks clean. @snokam/auth lints with 0 errors and its only
139
+ tsc output is a pre-existing tsconfig deprecation.
140
+
141
+ - @snokam/config is replaced by @snokam/tenant. The "./tenant"
142
+ subpath is removed; import host predicates from the root and backendUrl from
143
+ "@snokam/tenant/server".
144
+
145
+ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
146
+
147
+ - refactor(sanity-page-content)!: resolve the tenant once, not twice
148
+
149
+ The package had two complete, parallel tenant-resolution paths, both live:
150
+
151
+ - the HOCs (withSanityPageContent/withSanityAppContent, 85 call sites) took the
152
+ host from request headers and resolved credentials with a raw fetch
153
+ - makeTenantLayout/TenantPageContent (166 call sites) took the host from the
154
+ [host] route param and went through @snokam/config
155
+
156
+ Six apps ran both in one deployment — webshop-web's [host] pages used one and
157
+ its not-found.tsx the other — so the same request could resolve the same tenant
158
+ through two resolvers with two caches and two different error behaviours. And
159
+ makeTenantLayout hit the platform API twice for one host, because the layout and
160
+ the content component each did their own lookup.
161
+
162
+ There is now one resolveTenantContent. Both entry shapes are thin wrappers over
163
+ it and keep their exact signatures, so all 251 call sites are untouched. It
164
+ takes the host explicitly, which is what removes the double lookup: the layout
165
+ and the content component pass the same host and share one cached answer.
166
+
167
+ The two raw fetches are gone. server/tenant-credentials.ts and
168
+ server/platform-sanity.ts were hand-rolling calls the generated PlatformClient
169
+ already exposes as resolveTenantSanityCredentials and resolvePlatformSanityConfig;
170
+ both now live in @snokam/tenant and go through the generated client, inheriting
171
+ its timeout and stale-on-failure cache instead of each keeping their own.
172
+ server/request-host.ts is deleted in favour of the one in @snokam/tenant.
173
+
174
+ Dead code removed: the entire src/page-router tree (491 lines), MetaTags.tsx,
175
+ server.tsx, and six export subpaths with no consumers anywhere in the repo.
176
+
177
+ tsc on src goes from 28 errors to 21 — the seven that disappeared were in files
178
+ this deleted; all 21 remaining are the pre-existing stale-generated-sanity-client
179
+ errors under edit-mode/. Lint is 0 errors, warnings 14 to 11. The three changed
180
+ packages build.
181
+
182
+ Not verified: whole-app builds. @snokam/ui's build script calls rsync, which is
183
+ not available on this Windows machine, so `uv run start.py build <app>` cannot
184
+ complete here for reasons unrelated to this change. The route-level browser
185
+ check the plan asks for still needs to happen on a machine that can build.
186
+
187
+ - the ./server, ./providers and ./page-router* subpaths are
188
+ removed from @snokam/sanity-page-content, along with the page-router HOCs and
189
+ providers.
190
+
191
+ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
192
+
193
+ - refactor(tenant): fewer, fuller modules instead of one thought per file
194
+
195
+ The first pass traded a flat bag of files for a deep one. 19 files, seven of
196
+ them under 30 lines, and the folder layout did not survive its own commit
197
+ message: it claimed "the server.ts vs ./server name collision disappears" while
198
+ shipping both src/server.ts and src/server/.
199
+
200
+ What was ceremony rather than structure:
201
+
202
+ - platform.ts, 26 lines of which three were one-line env reads wrapped in a
203
+ function. Deleted; the two entries with actual logic
204
+ (isPlatformMarketingTenant, prospectRedirectUrl) moved next to the other host
205
+ questions, and platformApiBase — the one worth keeping, because it normalises
206
+ a trailing slash five former copies disagreed about — sits with the resolver
207
+ that needs it.
208
+ - urls/api-base.ts, seven exports wrapping a single domains.find. domainsOf was
209
+ literally `info?.domains ?? []` with one caller; hostedBackendUrl was a
210
+ template string. Collapsed into one `match` helper behind backendUrl/appUrl,
211
+ which are the two things callers actually ask for.
212
+ - resolve/{client,cache,by-host}.ts were one thought — resolve a tenant over the
213
+ network, with a cache — split three ways across 23, 28 and 133 lines.
214
+ - runtime/{env,transforms,local-urls}.ts likewise: the same question, "what
215
+ config does the browser get", in three files of 13, 78 and 58 lines.
216
+
217
+ Eleven files now, three of them entry points:
218
+
219
+ index.ts what a component imports
220
+ server.ts what a layout or page imports
221
+ middleware.ts the unknown-tenant proxy
222
+ tenant/ which tenant is this request (host, resolve, sanity)
223
+ urls/ what URL does a service have for it (index, domains)
224
+ config/ what config does the browser get (provider, resolve)
225
+
226
+ base-domain's fromHost/fromWindow/fromTenantInfo became
227
+ baseDomainFromHost/Window/Tenant, because the old names said nothing at the
228
+ call site once they were imported.
229
+
230
+ Typechecks clean; @snokam/auth and @snokam/sanity-page-content are unchanged at
231
+ 21 errors, all pre-existing.
232
+
233
+ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
234
+
235
+ - refactor: delete the wrappers that only restate their one line
236
+
237
+ An audit of every single-expression function in the changed packages, scored by
238
+ real call sites with .next build copies excluded, turned up four that existed
239
+ only to have a name:
240
+
241
+ - tenantDomains(info) was `info?.domains ?? []` with zero callers. It is the
242
+ same function this branch already deleted once as domainsOf, reintroduced two
243
+ commits later under a new name.
244
+ - studioUrlFor(host) was a template string with two callers, both of which now
245
+ build the URL where they use it.
246
+ - rewriteHost(host) in the tenant middleware was `effectiveHost(host) ?? null` —
247
+ a wrapper around the function that had just replaced it.
248
+ - OpenApiSetup's FallbackServers, ServiceName and IsLocal were three private
249
+ one-liners with one call site each, read once at the top of the method that
250
+ used them and now inlined as locals there.
251
+
252
+ Kept, because they carry a convention rather than a line of code: envKey names
253
+ the NEXT_PUBLIC_*_API_BASE_URL shape three callers depend on; requestInit is
254
+ where the timeout and revalidate window are decided for every platform call;
255
+ isHostname and bareHost are the host vocabulary the rest of the package speaks.
256
+
257
+ The expression-bodied members in Snokam.Tenancy and Snokam.Auth are pre-existing
258
+ code this branch moved, and they name a real API surface, so they are left alone.
259
+
260
+ The two requestHost copies in @snokam/auth look like duplicates but take
261
+ different inputs — a NextAuth req and a Request — so they are not folded here.
262
+ options.tsx is the live NextAuth config for every app and is not worth churning
263
+ for a shared helper of this size.
264
+
265
+ 0 build errors, 201 tests pass, typechecks unchanged.
266
+
267
+ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
268
+
269
+ - refactor: apply the same structural audit to every changed package
270
+
271
+ The tenant cleanup fixed one package. Auditing all seven against the same
272
+ criteria — entry-file size, files over 300 lines, single-file folders, and a
273
+ file sharing a name with a sibling folder — found the same defects elsewhere,
274
+ including two I had introduced.
275
+
276
+ @snokam/auth carried the collision twice over: server.tsx beside server/, and
277
+ route-handler.ts beside route-handler/. Both are now resolved, and the first one
278
+ by deletion: the ./server subpath had zero consumers, as did ./page-router, so
279
+ the Pages-Router tree the plan said to remove and this branch had left behind is
280
+ gone — server.tsx, server/auth.tsx, server/handler.tsx and all of page-router/.
281
+ route-handler.ts becomes route-handler/index.ts.
282
+
283
+ The root "." barrel exported page-router's Provider and Login as a default
284
+ object. Exactly one file in the repo imports from that entry, and it wants
285
+ buildAdminConsentUrl. The barrel now exports what is actually used.
286
+
287
+ options.tsx was still 362 lines, over the repo's limit, and the plan had called
288
+ for splitting it. Its first 137 lines were three request-header readers and an
289
+ employees-API profile fetch — neither of which is NextAuth configuration. They
290
+ move to options/request.ts and options/employee-profile.ts, leaving options/
291
+ index.tsx at 237 lines of providers, cookies and callbacks. Pure moves; no
292
+ behaviour touched, because this is the live NextAuth config for every app and it
293
+ is the one file in this branch I am least willing to rewrite.
294
+
295
+ Snokam.Tenancy had three single-file folders of my own making — Billing/, Jobs/
296
+ and Secrets/, one file each — against the repo rule that a folder holds two
297
+ files or a subfolder. Flattened. Snokam.Api/OAuth/ became single-file when
298
+ IPlatformOAuthTokens moved to Snokam.Tenancy, so its remaining file joins
299
+ Tenancy/ where its collaborators are.
300
+
301
+ Splitting options.tsx briefly recreated the very collision this commit removes
302
+ elsewhere (options.tsx beside options/); it is now options/index.tsx.
303
+
304
+ What the audit found and this leaves alone: Snokam.Api/Health/ and the
305
+ sanity-page-content single-file folders are pre-existing; the auth
306
+ components/<Name>/index.tsx folders are the ordinary React component-folder
307
+ convention; sanity.types.ts is generated.
308
+
309
+ 0 build errors, 201 tests pass, typechecks unchanged.
310
+
311
+ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
312
+
313
+ - refactor: close the three structural gaps the review found
314
+
315
+ A read of the PR as a newcomer would read it, rather than as a diff, turned up
316
+ three things this branch claimed to have done and had not.
317
+
318
+ The import cycle in sanity-page-content is real, not just structurally
319
+ discouraged. app-router/server.ts re-exports ./tenant/config and ./tenant/
320
+ resolve, while tenant/config.ts and tenant/content.tsx imported back through
321
+ that same barrel. Both now import ../../server/client directly, so nothing
322
+ inside the package imports the barrel and the cycle cannot come back by
323
+ accident.
324
+
325
+ build-proxy.tsx is what a reader finds when they follow @snokam/auth's
326
+ "./middleware" subpath. It is now middleware.tsx, named after the thing it is.
327
+
328
+ AdapterCapabilities and TenantCapabilities were two capability lists in two
329
+ packages, overlapping on accounting, crm, crypto-payout and consultant-cv —
330
+ exactly the duplicated-concept problem this whole branch is about, left in place
331
+ while other duplicates were removed. TenantCapabilities is gone and its two
332
+ unique values, ai-search and issue-tracking, join the surviving list.
333
+
334
+ The merge kept the name AdapterCapabilities rather than the shorter Capabilities
335
+ the plan proposed: notifications-function already has its own Capabilities class,
336
+ and the shorter name made every call site in that app ambiguous.
337
+
338
+ 0 build errors, 36 test projects pass, typechecks unchanged.
339
+
340
+ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
341
+
342
+ - refactor(api): make the authorization middleware read as its own sequence
343
+
344
+ FunctionAuthorizationMiddleware was 228 lines, and the file a newcomer has to
345
+ read to learn how a request is actually gated. Invoke interleaved the decision
346
+ sequence with the mechanics of each step: scheme selection and the 401s, tenant
347
+ binding from claims, scope-grant resolution, the impersonation audit, and two
348
+ different notions of "authorized" all sat inline.
349
+
350
+ The steps are now named, in Authorization/Pipeline/:
351
+
352
+ RequestAuthentication pick the scheme, authenticate, bind the tenant, and
353
+ write the 401 or 403 itself when the caller cannot pass
354
+ TenantFromClaims bind the tenant the caller's claims name, and the
355
+ business match
356
+ ScopeGrants baseline scopes plus what this caller has been granted
357
+ ImpersonationAudit is this impersonated, and the audit record
358
+
359
+ Invoke is now the sequence and nothing else, and the two notions of authorized
360
+ are separated: a scope gate checks granted scopes, everything else runs the
361
+ policy — with the reason an API key cannot satisfy a policy gate stated where
362
+ that decision is made rather than left as a bare `authorized = false`.
363
+
364
+ The four members the tests reached into the middleware for — SelectScheme,
365
+ ResolveEmail, IsImpersonation and the RequiredScopeItem key — moved with the
366
+ behaviour they belong to, and the tests now call them on the unit that owns
367
+ them rather than on the middleware.
368
+
369
+ Also documents, in backend/packages/README.md, the rule the package split
370
+ exists to protect: .Application and .Domain may reference the leaves but never
371
+ Snokam.Api, which is why Snokam.Auth holds the vocabulary you declare
372
+ authorization with while the middleware that enforces it lives in the host. The
373
+ README names Employees.Application as an existing violation of that rule.
374
+
375
+ 201 tests pass.
376
+
377
+ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
378
+
379
+ - fix: repair the build breaks this branch shipped
380
+
381
+ A max-effort review type-checked real apps, which this branch never did, and
382
+ found it does not build: 16 of 16 apps failed. Every per-package `tsc --noEmit`
383
+ I ran passed, because a package's own sources compiled fine — the breakage was
384
+ entirely in what consumers import. The codemod rewrote import paths and left the
385
+ symbols behind them unchecked.
386
+
387
+ Four breaks, all introduced here:
388
+
389
+ - options/index.tsx used isColorScheme and ColorScheme after the split moved
390
+ them to options/employee-profile.ts, without importing them, and
391
+ isColorScheme was not exported. Line 223 is the NextAuth session callback, so
392
+ this was a ReferenceError on every session read — platform-wide logout.
393
+ - resolveTenantApiBase was deleted with @snokam/config while 10 files across 6
394
+ apps were rewritten to import it from @snokam/tenant. It is back with its
395
+ original argument order, on ./server where its managed-identity dependency
396
+ belongs, and those files now import it from there.
397
+ - buildUnknownTenantProxy moved behind ./middleware while all 15 app proxy.ts
398
+ files import it from the root. Restored to the root.
399
+ - The generated TenantSanityCredentials and PlatformSanityConfig have every
400
+ field nullable, so six assignments stopped type-checking. Both are narrowed
401
+ once at their boundary in tenant/sanity.ts rather than at each use.
402
+
403
+ The review also caught that the client barrel had begun re-exporting
404
+ server-only modules: tenant/host.ts imported next/headers and tenant/resolve.ts
405
+ pulled @snokam/identity, so ~50 "use client" files had next/headers and the
406
+ Azure SDK in their import graph. host.ts is pure again and requestHost,
407
+ apiScope and managedBearer live under server/. That also makes the Edge
408
+ middleware safe, which it was not.
409
+
410
+ Two backend findings from the same review:
411
+
412
+ - OpenApiSetup computed the server list eagerly in ConfigureServices after the
413
+ ConfigureApi split, so a malformed BASE_PATH or host-name setting would abort
414
+ IHost.Build() and take every trigger down, where before it only broke the
415
+ swagger route. It is lazy inside the factory again.
416
+ - EndpointGates.FunctionMethods had a whole-assembly catch-all the old
417
+ SecuritySchemeDocumentFilter did not, which would silently publish every
418
+ protected endpoint as unsecured. Removed; the per-type catch stays.
419
+
420
+ The remaining app type errors are pre-existing and in files this branch does
421
+ not touch, confirmed per file against origin/main.
422
+
423
+ 201 tests pass.
424
+
425
+ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
426
+
427
+ - fix: address the behaviour regressions the review found
428
+
429
+ Seven findings, five of them regressions this branch introduced against main.
430
+
431
+ A tenant's Sanity read token was being written to disk. The consolidation gave
432
+ every platform call the same requestInit(), including next: { revalidate: 300 },
433
+ where the deleted tenant-credentials.ts deliberately used cache: "no-store" —
434
+ Next's Data Cache persists the body, secret included. Responses carrying a
435
+ secret now go through secretRequestInit(), which does not cache.
436
+
437
+ The 3-attempt retry with backoff was dropped in the same consolidation, leaving
438
+ one call under a 2s budget. A platform cold start routinely exceeds that, so the
439
+ first request after scale-to-zero hit the error page where it used to self-heal
440
+ on attempt 2. Restored, still one attempt when a stale entry is already in hand.
441
+
442
+ An unavailable resolution was being written into the 5-minute cache, which the
443
+ old code explicitly guarded against. One incomplete payload — a read token not
444
+ yet provisioned, a half-rotated secret — pinned that tenant to the error page
445
+ for five minutes after the platform recovered. Only a resolved or not-found
446
+ answer is cached now.
447
+
448
+ The credentials cache was unbounded and keyed on a caller-influenceable Host
449
+ header, so varied hosts grew the heap for the life of the container, each entry
450
+ holding a Sanity read token. It now caps and evicts like its sibling one file
451
+ over.
452
+
453
+ resolveTenantSanityConfig passes an all-empty baseConfig, and the resolver
454
+ eagerly built a fallback client from it, so @sanity/client threw "Configuration
455
+ must contain 'projectId'" — a hard 500 on a tenant whose content had resolved
456
+ fine. A client is only built from baseConfig when it actually names a project.
457
+
458
+ The same function lost its anonymous fallback: a managed-identity hiccup or an
459
+ unset IDENTITY_AD_SCOPE produced not-tenant-scoped, which now threw, and
460
+ handleTenantError turned that into a redirect to the prospect signup page for a
461
+ live paying tenant. resolveTenantContent takes allowAnonymous again and
462
+ resolves the business through resolveTenantByHostStrict — which the review
463
+ noted had been left exported with no callers, exactly because this path was
464
+ deleted.
465
+
466
+ studioUrl was overwritten with https://<host>/admin even when the caller named a
467
+ dedicated studio, so stega edit links pointed somewhere the HOCs' own
468
+ previewClientConfig did not. The caller's value wins again.
469
+
470
+ backendUrl's Entra-tenant-id fallback had narrowed from "this tenant has no
471
+ employees entry" to "this tenant has no domains at all", silently costing the
472
+ avatar and colour scheme at sign-in; it is keyed on the wanted entry again. Its
473
+ configured-base tail, dropped in the rewrite, is also back.
474
+
475
+ Also merges server/{request-host,token}.ts — 9 and 10 lines — into one
476
+ server/request.ts, having reintroduced the same one-thought-per-file pattern
477
+ while fixing the client-graph split.
478
+
479
+ Verified where it matters this time: five apps type-check with zero symbol or
480
+ module errors from this branch.
481
+
482
+ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
483
+
484
+ - fix: restore a stripped dependency, revert unrelated collateral, and lint the new package
485
+
486
+ Four findings from the review's gap sweep.
487
+
488
+ The PackageReference cleanup deleted Employees.Infrastructure's direct
489
+ Microsoft.AspNet.WebApi.Client 6.0.0 — a reference that was there on main, not
490
+ one this branch added. The lock file shows the silent downgrade: Direct 6.0.0
491
+ became Transitive 5.2.8, surviving only because a generated client happens to
492
+ drag that version in. Cv.cs still calls ReadAsAsync<T> at two call sites. The
493
+ exact inverse of this branch's "seven projects now declare what they use" claim,
494
+ in the same commit that made it.
495
+
496
+ apps/my-page-native/packages.lock.json had been regenerated on this Windows
497
+ machine, replacing every iOS and Mac Catalyst target frame with Windows and
498
+ Android ones. native-app-release.yml publishes -f net10.0-ios and
499
+ -f net10.0-maccatalyst on macOS, so that lock either silently rewrites itself or
500
+ hard-fails NU1004 under locked-mode restore. A MAUI app has nothing to do with
501
+ this refactor; reverted to main.
502
+
503
+ Four committed files failed prettier --check, which is both the pre-commit hook
504
+ and the pnpm check CI gate — the direct consequence of every commit here using
505
+ --no-verify. Every file this branch touches now passes.
506
+
507
+ That bypass had a cause worth fixing rather than repeating: lint-staged handed
508
+ prettier a "*" glob, so a .cs-heavy commit passed hundreds of paths it does not
509
+ format and blew the Windows command-line limit. The glob now lists the
510
+ extensions prettier actually handles, and this commit is made with the hook
511
+ running.
512
+
513
+ @snokam/tenant had no lint script and no eslint config, so turbo run lint — the
514
+ authoritative CI gate — skipped the whole workspace, while ~500 lines of moved
515
+ logic landed in it and tsc ran with --noCheck. That hole is why several review
516
+ findings had to be caught by hand. It now has both, and the first run found a
517
+ non-null assertion in isKnownTenantHost, now gone.
518
+
519
+ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
520
+
521
+ - fix: close the remaining packaging and studio-URL findings
522
+
523
+ Four items from the review's final pass, plus one regression of my own it
524
+ surfaced.
525
+
526
+ @snokam/auth's publishConfig pointed every entry at ./dist/*, for a package
527
+ with no build script that ships only src — so the next release would have
528
+ published entry points that are not in the tarball. On main there was no
529
+ publishConfig.exports at all and resolution fell through to the src-based
530
+ exports map; I added the dist one in this branch and turned a working published
531
+ package into a broken one. Removed, back to {access: "public"}.
532
+
533
+ @snokam/tenant was at 1.0.0, below the @snokam/config@1.1.5 it replaces, so a
534
+ consumer on ^1.1.0 could not resolve the successor and semver called the new
535
+ package older. It is 1.2.0.
536
+
537
+ The custom-domain studio URL is properly restored. studioUrlFor already honoured
538
+ an explicit baseConfig.studioUrl, but the anonymous path then discarded the
539
+ tenant record it had just fetched. It now derives https://admin.<base> from that
540
+ record, which is what the deleted resolveStudioUrl(host, info) did and the only
541
+ reason baseDomainFromTenant was ported — it had been sitting with no consumers.
542
+ The credentials path keeps https://<host>/admin, matching main, where
543
+ resolveStudioUrl was called without info there.
544
+
545
+ Two test projects' lock files still declared the deleted Snokam.Security while
546
+ the other 68 were regenerated. Both restored; the only remaining mentions of
547
+ that name in the repo are two workflow files and AGENTS.md, which is
548
+ documentation to update separately.
549
+
550
+ Not addressed, and the one item that should block a merge: the API-key path has
551
+ no tenant-vs-caller assertion, so [FunctionAuthorize(Business=...)] fails open
552
+ for a foreign key whose business does not resolve. It is pre-existing rather
553
+ than introduced here, and closing it rejects API keys that currently work, so it
554
+ needs a decision rather than a refactor commit.
555
+
556
+ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
557
+
558
+ - fix: regenerate the employees lock so the restored reference is reflected
559
+
560
+ The earlier fix put Microsoft.AspNet.WebApi.Client 6.0.0 back in
561
+ Employees.Infrastructure.csproj but left its packages.lock.json still recording
562
+ the package as Transitive 5.2.8. A half-fix: the project and its lock disagreed,
563
+ which under the RestoreLockedMode=true that Directory.Build.props sets in CI is
564
+ an NU1004 rather than the silent downgrade it was before. Regenerated; the entry
565
+ is Direct 6.0.0 again.
566
+
567
+ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
568
+
569
+ - docs: retire the old package names from prose and record the dropped default export
570
+
571
+ The rename left six stale references outside code, all of which a reader would
572
+ follow to a package that no longer exists: AGENTS.md's shared-package list, the
573
+ architecture table in development-web's internal docs, a comment in
574
+ tooling/core/run/frontend/manager.py pointing at "@snokam/config resolver", the
575
+ Snokam.Security examples in Backend.Tests.proj and the two deploy workflows, and
576
+ an iac comment describing a Snokam.Security.GroupGuard that is now a scope gate.
577
+ The only surviving mentions are CHANGELOG history, which is correct to keep.
578
+
579
+ Also records a breaking change this branch made silently. @snokam/auth's root
580
+ entry exported `default { Login, Error, Provider, Authenticate }`; the new
581
+ index.ts has no default export, because Login and Provider came from the
582
+ page-router tree this branch deletes. Nothing in the monorepo imports the
583
+ default — every Auth.Provider usage comes from @snokam/auth/app-router, whose
584
+ default export is unchanged — but the package is published, so an external
585
+ `import Auth from "@snokam/auth"` now gets undefined.
586
+
587
+ It stays dropped rather than restored: pointing a default at the app-router
588
+ components would hand external callers different components under the same
589
+ names, which is worse than an honest break. It belongs in the release notes,
590
+ which is what this commit is for.
591
+
592
+ - @snokam/auth no longer has a default export on its root entry.
593
+ Import the named components from @snokam/auth/app-router instead.
594
+
595
+ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
596
+
597
+ - fix: keep a reachable tenant reachable when the platform stumbles
598
+
599
+ Three separate failures all collapsed into "business not found", the one
600
+ outcome wired to a signup redirect, or into a silent 200 of placeholders:
601
+
602
+ - anonymousClient swallowed every error, so a managed-identity hiccup
603
+ redirected a live tenant's visitors to /kom-i-gang. A resolver that
604
+ could not answer is now ServiceUnavailableError; only a real 404 is
605
+ not-found, and a business with no Sanity project says so by name.
606
+ - A Sanity 401/403 counted as "this page has no content" and rendered
607
+ Placeholder for {key} at HTTP 200. It now fails like an unreachable
608
+ dataset, which is what it is.
609
+ - resolveTenantSanityCredentials dropped serve-stale on an incomplete
610
+ payload; a warm tenant keeps its last-known-good credentials.
611
+
612
+ Also: an app serving its own project keeps its own dataset and token
613
+ instead of taking the platform's copy; the missing-host sentinel stops
614
+ becoming a real lookup that pins a bogus cache entry; platformLayer
615
+ folds into resolvePlatformLayerConfig so the two cannot drift, and a
616
+ negative platform answer is remembered for the retry cooldown.
617
+
618
+ The gate guard no longer swallows a type it cannot reflect over, which
619
+ let its endpoints ship ungated while startup reported the app clean.
620
+ It now fails closed on both reflection paths.
621
+
622
+ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
623
+
624
+ ## [2.1.11](https://github.com/snokam/monorepo/compare/@snokam/navbar@2.1.10...@snokam/navbar@2.1.11) (2026-09-21)
625
+
626
+ **Note:** Version bump only for package @snokam/navbar
627
+
628
+ ## [2.1.10](https://github.com/snokam/monorepo/compare/@snokam/navbar@2.1.9...@snokam/navbar@2.1.10) (2026-09-02)
629
+
630
+ **Note:** Version bump only for package @snokam/navbar
631
+
632
+ ## [2.1.9](https://github.com/snokam/monorepo/compare/@snokam/navbar@2.1.8...@snokam/navbar@2.1.9) (2026-08-28)
633
+
634
+ **Note:** Version bump only for package @snokam/navbar
635
+
636
+ ## [2.1.8](https://github.com/snokam/monorepo/compare/@snokam/navbar@2.1.7...@snokam/navbar@2.1.8) (2026-08-28)
637
+
638
+ **Note:** Version bump only for package @snokam/navbar
639
+
640
+ ## [2.1.7](https://github.com/snokam/monorepo/compare/@snokam/navbar@2.1.6...@snokam/navbar@2.1.7) (2026-08-28)
641
+
642
+ **Note:** Version bump only for package @snokam/navbar
643
+
644
+ ## [2.1.6](https://github.com/snokam/monorepo/compare/@snokam/navbar@2.1.5...@snokam/navbar@2.1.6) (2026-08-27)
645
+
646
+ **Note:** Version bump only for package @snokam/navbar
647
+
648
+ ## [2.1.5](https://github.com/snokam/monorepo/compare/@snokam/navbar@2.1.4...@snokam/navbar@2.1.5) (2026-08-25)
649
+
650
+ **Note:** Version bump only for package @snokam/navbar
651
+
6
652
  ## [2.1.4](https://github.com/snokam/monorepo/compare/@snokam/navbar@2.1.3...@snokam/navbar@2.1.4) (2026-08-18)
7
653
 
8
654
  ### Bug Fixes
@@ -6,7 +6,7 @@ import { HeaderTheme } from "./types";
6
6
  import Layout from "@snokam/ui/layout";
7
7
  import { useColorScheme } from "@snokam/ui/theme/admin";
8
8
  import { useAuth } from "@snokam/auth/app-router";
9
- import { useConfig } from "@snokam/config";
9
+ import { useConfig } from "@snokam/tenant";
10
10
  import { SystemThemeTag } from "@snokam/ui";
11
11
  import { LanguagePicker } from "../LanguagePicker/index.app-router";
12
12
  import { useFramework } from "@snokam/ui/framework";
@@ -4,7 +4,7 @@ import { usePageContent } from "@snokam/sanity-page-content";
4
4
  import { HeaderTheme } from "./types";
5
5
  import { useColorScheme } from "@snokam/ui/theme/admin";
6
6
  import { useAuth } from "@snokam/auth";
7
- import { useConfig } from "@snokam/config";
7
+ import { useConfig } from "@snokam/tenant";
8
8
  import { LanguagePicker } from "../LanguagePicker";
9
9
  import { SimpleHeaderBase } from "./base";
10
10
  /**
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.1.4",
2
+ "version": "3.0.1",
3
3
  "dependencies": {
4
4
  "@emotion/react": "^11.14.0",
5
5
  "@emotion/styled": "^11.14.1",
@@ -10,17 +10,17 @@
10
10
  "next-auth": "4.24.15",
11
11
  "react": "19.2.8",
12
12
  "react-dom": "19.2.8",
13
- "@snokam/auth": "2.3.2",
14
- "@snokam/config": "1.1.0",
15
- "@snokam/ui": "1.1.1",
16
- "@snokam/sanity-page-content": "1.18.0"
13
+ "@snokam/sanity-page-content": "3.0.1",
14
+ "@snokam/tenant": "2.0.0",
15
+ "@snokam/auth": "3.0.1",
16
+ "@snokam/ui": "2.0.2"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/node": "^26.1.1",
20
20
  "@types/react": "^19.2.17",
21
21
  "@types/react-dom": "^19.2.3",
22
22
  "eslint": "^10.7.0",
23
- "next": "16.2.11",
23
+ "next": "16.3.3",
24
24
  "typescript": "5.9.3"
25
25
  },
26
26
  "name": "@snokam/navbar",