@snokam/navbar 2.1.5 → 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,648 @@
|
|
|
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
|
+
|
|
6
648
|
## [2.1.5](https://github.com/snokam/monorepo/compare/@snokam/navbar@2.1.4...@snokam/navbar@2.1.5) (2026-08-25)
|
|
7
649
|
|
|
8
650
|
**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/
|
|
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/
|
|
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
|
+
"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/
|
|
14
|
-
"@snokam/
|
|
15
|
-
"@snokam/
|
|
16
|
-
"@snokam/ui": "
|
|
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.3.
|
|
23
|
+
"next": "16.3.3",
|
|
24
24
|
"typescript": "5.9.3"
|
|
25
25
|
},
|
|
26
26
|
"name": "@snokam/navbar",
|