@snokam/navbar 2.1.5 → 3.0.2

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