Repository navigation
Expand file tree
/
Copy pathappsettings.example.jsonc
More file actions
612 lines (555 loc) · 39.8 KB
/
Copy pathappsettings.example.jsonc
File metadata and controls
612 lines (555 loc) · 39.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
// ─────────────────────────────────────────────────────────────────────────────────────────────────────
// CRAFT — annotated configuration reference.
//
// THIS FILE IS NOT LOADED. It is documentation. CRAFT ships no appsettings.json: the Craft.csproj
// content glob is `appsettings*.json`, which this `.jsonc` name deliberately does not match, so nothing
// here reaches the published output or the container image.
//
// It is `.jsonc` rather than `.json` because it contains comments. ASP.NET Core's JSON configuration
// provider happens to tolerate them (it parses with JsonCommentHandling.Skip), but they are not valid
// JSON per the spec, and a file named `.json` that no generic tool, schema validator or linter can read
// is a trap. The extension now matches the contents.
//
// TO CONFIGURE A DEPLOYMENT, pick one:
// 1. Environment variables — App__Worker__HttpPoolSize=4, App__Auth__CookieName=..., and the CRAFT_*
// shorthands. This is what the E2E harness and most deployments use. Highest precedence.
// 2. Your own appsettings.json, supplied by the downstream app image (COPY it to /app) or dropped in
// the project root for local `dotnet run`.
//
// EVERY VALUE BELOW IS ALREADY THE BUILT-IN DEFAULT. The defaults are the C# property initialisers in
// Services/CraftSettings.cs — that file is the single source of truth. Copy out only the keys you are
// actually changing; restating a default here just creates something that silently goes stale when the
// default moves.
//
// Full prose documentation: docs/configuration.md
// ─────────────────────────────────────────────────────────────────────────────────────────────────────
{
// Framework-level (root, NOT under "App"). Both shown here are the ASP.NET Core defaults.
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"App": {
// Display name shown in logs and diagnostics
"Name": "Craft",
// ── Deployment roles (capabilities) ──
// One image, three independent switches for split deployments. Leave all unset (the default) to run the
// full monolith: Frontend + Http + Background. If you set ANY of the three, the host uses exactly those
// (unset → off). Environment overrides (take precedence): CRAFT_SERVE_FRONTEND, CRAFT_SERVE_API,
// CRAFT_RUN_BACKGROUND (each true/false).
// Presets: frontend | http | background | backend (http+bg) | frontend+http | combined (default).
// NOTE: a node that triggers orchestrations from HTTP must also carry Background (use "backend"/"combined").
// "Roles": { "Frontend": true, "Http": true, "Background": false },
// ── Health probe ──
// Role-agnostic liveness/readiness endpoint. Enabled by default at /healthz. Relocate or disable it via
// App:Health:Path / App:Health:Enabled, or the CRAFT_HEALTH_PATH / CRAFT_HEALTH_ENABLED env vars (win).
// "Health": { "Enabled": true, "Path": "/healthz" },
// Controls when Kestrel starts accepting connections:
// "Immediate" — Kestrel starts first, init runs in background (default, fast startup)
// "HttpReady" — Kestrel starts after HTTP worker pool is ready
// "AllReady" — Kestrel starts after all worker pools are fully initialized
// "ReadinessMode": "Immediate",
// Kestrel request timeout (seconds). Determines how long Kestrel waits for a complete
// request before aborting. If not set (or 0), derives from Worker.HttpTimeoutSeconds
// if > 0, else defaults to 600 (10 minutes). Recommended: set slightly higher than
// HttpTimeoutSeconds to give workers time to respond before Kestrel drops the connection.
// "KestrelTimeoutSeconds": 130,
// ── Kestrel request limits (DoS protection) ──
// Applied unconditionally at startup (independent of the timeout above). Values shown are
// the defaults — no config needed.
// "Limits": {
// // Max request body size in MB. Default 100. Set to 0 for unlimited (not recommended).
// "MaxRequestBodyMB": 100,
// // Max concurrent TCP connections. Default 200. Set to 0 or a negative value for unlimited.
// "MaxConcurrentConnections": 200
// },
// ── Request rate limiting ──
// Per-client fixed-window limiter. ENABLED BY DEFAULT (300 requests / 10s per client),
// partitioned by the authenticated principal name (x-ms-client-principal-name), falling back to
// the first X-Forwarded-For hop so anonymous callers behind the App Service load balancer aren't
// collapsed into a single bucket. Requests over the limit get HTTP 429 with a Retry-After header.
// Set "Enabled": false to turn it off (e.g. for load/perf testing); CRAFT_RATELIMIT_ENABLED=true
// can also force it on.
//
// ApiConcurrencyLimit adds a second, orthogonal cap: the most requests a single app-only API
// client (client-credentials caller, idp=aad) may have occupying the worker system AT ONCE —
// counting both those queued waiting for a runspace and those executing, since the limiter's lease
// spans the whole downstream pipeline. Keyed per client (its AppId), so one automation cannot hold
// every runspace and starve the interactive UI, which is never capped by this. A rate cap
// (PermitPerWindow) bounds requests/second; this bounds simultaneous-in-flight. 0 (default) = off.
// Over-limit requests are rejected immediately with 429 + Retry-After (no concurrency queue).
// Env override: CRAFT_API_CONCURRENCY_LIMIT. Works even with "Enabled": false (rate limiter off,
// concurrency cap on) — the limiter middleware runs whenever either is active.
// "RateLimit": {
// "Enabled": true,
// "PermitPerWindow": 300, // requests allowed per window, per client
// "WindowSeconds": 10, // window length in seconds
// "QueueLimit": 0, // requests queued at the limit before rejecting (0 = reject immediately)
// "ApiConcurrencyLimit": 0, // max simultaneous in-flight requests per API client (0 = unlimited)
//
// // Egress: a THIRD, orthogonal cap — outbound response bytes/day for the WHOLE instance, counting
// // only app-only API clients (UI is never counted or capped). A caller can pull tens of GB/day
// // while staying under the rate and concurrency caps (one slow, large request every few seconds),
// // which is what this bounds. Two-step by design:
// // 1. Accounting turns on when the env named by HostedEnv (default CIPP_HOSTED — the same flag
// // the SKU profiles use) is present, or CRAFT_API_EGRESS_LIMIT_ENABLED forces it (true = on
// // even off-host for testing, false = kill switch even when hosted).
// // 2. Enforcement (429 + Retry-After to the next UTC midnight) begins only once BytesPerDay > 0,
// // so you can run accounting-only first and watch real numbers before rejecting anything.
// // The counter lives in memory (no per-request IO) and is flushed to egress-ledger.json in the same
// // directory as the log files every FlushSeconds and on shutdown, so a restart doesn't reset the day.
// // The file holds per-client daily totals + the cap-hit state (the source of truth for the cap).
// // In addition, per-client + instance rows are mirrored to the TableName table — 15-minute buckets
// // (the time-series) and a daily audit row (cap in force, when the cap was first hit, how many
// // requests were shed) — so the product can chart usage and show cap hits. The table is the same
// // storage account the hosted app reads with Get-CIPPTable, retained RetentionDays and purged
// // periodically. A restart reseeds the current bucket from the table so its row doesn't regress.
// // Env overrides: CRAFT_API_EGRESS_LIMIT_BYTES, CRAFT_API_EGRESS_FLUSH_SECONDS, CRAFT_API_EGRESS_TABLE
// // (blank = file-only), CRAFT_API_EGRESS_BUCKET_MINUTES, CRAFT_API_EGRESS_RETENTION_DAYS.
// "Egress": {
// "HostedEnv": "CIPP_HOSTED", // env var whose presence enables accounting ("" = never auto-enable)
// "BytesPerDay": 0, // per-instance daily budget in bytes (0 = accounting only, no 429s; 1 GB = 1073741824)
// "FlushSeconds": 60, // how often the counter is written to disk + mirrored to the table (also on shutdown)
// "TableName": "CraftEgressAccounting", // table for the bucketed history + daily audit ("" = file only)
// "BucketMinutes": 15, // width of each history bucket (→ 96 buckets/day)
// "RetentionDays": 7 // days of table rows kept before the periodic purge
// }
// },
// Realtime SSE channel served at /.craft/events. Downstream code publishes job events through
// [Craft.Services.RealtimeBridge]::Publish($userId,$jobId,"start|update|end",$data); browsers consume
// them. In-memory, single instance.
// OFF BY DEFAULT — set "Enabled": true below to turn it on (env override, wins: CRAFT_REALTIME_ENABLED
// =true/false). While off, /.craft/events is not mapped and Publish calls are no-ops.
"Realtime": {
"Enabled": false
// Tuning (values shown are the defaults — no config needed):
// "MaxMessageBytes": 16384, // per-event data cap; over this the payload is dropped + a 413 frame sent
// "MaxActiveJobs": 10000, // max stored (userId, jobId) entries
// "MaxConnections": 1000, // max concurrent SSE streams
// "PerConnectionQueue": 256, // buffered frames per connection before oldest is dropped
// "HeartbeatSeconds": 20, // keep-alive comment interval
// "EntryTtlMinutes": 60 // backstop eviction for jobs that never send "end"
},
// File-backed logging with rotation (all values below are defaults — no config needed)
// "FileLogging": {
// "Directory": "", // default: /logs (Linux) or {BaseDir}/logs (Windows)
// "FilePrefix": "craft", // files: craft.log, craft.1.log, craft.2.log, ...
// "MaxFileSizeMB": 10, // rotate when current file exceeds this size
// "MaxFileCount": 10, // keep at most N rotated files (total: N+1 including current)
// "TimestampFormat": "yyyy-MM-ddTHH:mm:ss.fffZ",
// "IncludeCategory": false, // include logger category name in log lines
// "LogLevel": "Information" // Trace|Debug|Information|Warning|Error|Critical
// // Also controls PS stream capture:
// // Debug → enables Write-Debug
// // Trace → enables Write-Debug + Write-Verbose
// // Override via CRAFT_LOG_LEVEL env var
// },
"Worker": {
// Number of dedicated HTTP request workers (each handles one request at a time).
// 0 = build no HTTP runspaces at all — for apps whose routes are all native C# endpoints.
"HttpPoolSize": 2,
// Number of background workers for scheduler, orchestrator, and queue tasks.
// 0 = build no BG runspaces at all — for apps whose scheduled work is all native C# tasks
// (they run on the .NET thread pool; the scheduler and JobManager still start). With both
// sizes 0 the container hosts no PowerShell whatsoever.
"BgPoolSize": 4,
// Run each worker's PowerShell pipeline on one reused thread instead of a new thread per invocation.
// Default true — the biggest per-request dispatch win (~50% of PS-invoke cost; see
// docs/dispatch-analysis.md). Set false only to A/B or if a module misbehaves on a long-lived thread.
// "ReuseRunspaceThread": true,
// Minutes between timed memory trims (compacting full GC). 0 = disabled. Default 5.
// "MemoryTrimIntervalMinutes": 5,
// Host-tier pool sizing. When set, the first matching entry overrides
// HttpPoolSize/BgPoolSize above based on the runtime environment.
// SkuEnv: name of the env var to read for the host tier identifier
// (e.g. "WEBSITE_SKU" on Azure App Service). Omit or "" = wildcard.
// Sku: expected value of that env var, compared case-insensitively
// (e.g. "Basic", "PremiumV3"). Ignored when SkuEnv is empty.
// Cpu: compared to Environment.ProcessorCount (respects cgroup limits in
// Docker). Omit or 0 = match any CPU count.
// GCHeapHardLimitMB: optional GC heap hard limit (MB) for this tier. The
// DOTNET_GCHeapHardLimit env var is consumed before managed code runs,
// so this lands via GC.RefreshMemoryLimit at startup instead. Three-way,
// mirroring the CRAFT_GC_HEAP_LIMIT_MB override below: omit/null (or a
// negative value) = keep the process baseline; 0 = disable the cap entirely,
// deferring to container memory (for a tier with more RAM than the baked
// limit lets it use); > 0 = set that many MB. Raising or removing is always
// safe; a positive value the heap has already outgrown is refused and logged.
// Per-instance override: CRAFT_GC_HEAP_LIMIT_MB wins over this (a positive
// value sets the cap; 0 disables the cap entirely, deferring to container
// memory) — hand-tune one host without touching the fleet-wide profiles.
// First match wins. No match (or any parse failure) leaves the baseline above.
// Set IgnoreSkuProfiles: true to disable this entirely.
// "IgnoreSkuProfiles": false,
// "SkuProfiles": [
// { "SkuEnv": "WEBSITE_SKU", "Sku": "Basic", "Cpu": 1, "HttpPoolSize": 2, "BgPoolSize": 2 },
// { "SkuEnv": "WEBSITE_SKU", "Sku": "Basic", "Cpu": 2, "HttpPoolSize": 3, "BgPoolSize": 4 },
// { "SkuEnv": "WEBSITE_SKU", "Sku": "Basic", "Cpu": 4, "HttpPoolSize": 4, "BgPoolSize": 4, "GCHeapHardLimitMB": 5120 },
// { "SkuEnv": "WEBSITE_SKU", "Sku": "PremiumV3", "Cpu": 2, "HttpPoolSize": 4, "BgPoolSize": 8 },
// { "Cpu": 8, "HttpPoolSize": 6, "BgPoolSize": 12 }
// ],
//
// A second SkuProfiles matrix, selected by the *presence* of an env var. When the var named by
// SkuProfilesAltEnv is set to any non-empty value, SkuProfilesAlt is matched instead of SkuProfiles
// — e.g. a smaller per-instance sizing for instances packed onto one shared App Service Plan. Falls
// back to SkuProfiles when the var is unset or SkuProfilesAlt is empty. Same fields as SkuProfiles.
// Set the var on the packed instances and leave it unset on dedicated ones. As with SkuProfiles, an
// entry that matches nothing just keeps the global HttpPoolSize/BgPoolSize — no catch-all needed.
// "SkuProfilesAltEnv": "CIPP_HOSTED",
// "SkuProfilesAlt": [
// { "SkuEnv": "WEBSITE_SKU", "Sku": "PremiumV3", "Cpu": 8, "HttpPoolSize": 2, "BgPoolSize": 4, "GCHeapHardLimitMB": 2048 }
// ],
//
// Per-instance overrides for the pool sizes themselves, winning over any matched profile (same
// shape as CRAFT_GC_HEAP_LIMIT_MB): CRAFT_HTTP_POOL_SIZE / CRAFT_BG_POOL_SIZE. A non-negative
// integer wins; unset / negative / unparseable defers to the matrix. Hand-tune one host without
// touching the fleet-wide profiles.
// Maximum execution time (seconds) for HTTP request handlers
// When exceeded, the PowerShell pipeline is stopped and the worker is reclaimed
// 0 = no timeout (default). Recommended: 120-300 for HTTP endpoints
// "HttpTimeoutSeconds": 0,
// Maximum execution time (seconds) for background jobs (scheduler, orchestrator tasks)
// When exceeded, the PowerShell pipeline is stopped and the worker is reclaimed
// 0 = no timeout (default). Recommended: 600-3600 for background jobs
// "BgTimeoutSeconds": 0,
// Grace (seconds) a timed-out or cancelled pipeline gets to stop. A pipeline blocked inside a .NET
// call never sees the stop; past this grace its worker is abandoned and replaced, and the caller
// gets the usual timeout. /healthz reports workers.hung / workers.hungTotal.
// "StopGraceSeconds": 30,
// How long an HTTP request waits for a free runspace when the pool is saturated before it is
// shed with 503 "Server busy, please retry". A load-shedding bound, NOT a capacity knob — it
// does not add throughput, it only decides how long callers wait before a spurious 503. Widen
// it to absorb brief bursts; raise HttpPoolSize/MinThreads for steady saturation. Distinct from
// HttpTimeoutSeconds (which bounds execution once a worker is held). Waiters are served in two
// FIFO tiers: interactive users first, then API clients and background cache refreshes.
// 0 = built-in default of 30s. Env override: CRAFT_HTTP_QUEUE_TIMEOUT (seconds).
// "HttpQueueTimeoutSeconds": 30,
// Extra env vars injected into every runspace
// "EnvVars": { "MY_VAR": "value" },
// Additional env var names set to the API root path (alongside $env:CRAFT_ROOT)
// "RootPathVars": ["MyAppRoot"],
// Scripts run once on first worker for warmup (after module import, errors non-fatal)
// "WarmupScripts": ["Get-MyAuth | Out-Null"],
// .NET assemblies loaded into each runspace (paths relative to API/)
// "SharedAssemblies": ["Shared/MyLib.dll"],
// Inject shared Synchronized Hashtables into module scopes for cross-runspace state
// "ModuleInjections": [{ "Module": "MyModule", "Variable": "tokenCache", "CacheKey": "TokenCache" }],
// Scripts run after init on EVERY worker (not just the first)
// "PostInitScripts": [],
// Module directory names to skip during import
// "SkipModules": [],
// Minimum .NET thread-pool threads. 0 (default) = derive from the pool sizes:
// max(cores*4, 32, HttpPoolSize + BgPoolSize + 16).
//
// PowerShell has no async story — every outbound call a script makes blocks a thread for the
// whole round trip, so N workers can park N threads at once. Above this minimum the CLR injects
// threads at roughly ONE PER SECOND, so a worker pool larger than the minimum cannot reach its
// own concurrency until that ramp finishes, and pays it again on every restart.
// Measured on 1 core with a fixed floor of 32: a pool of 48 served 5.5 req/s over a 15s window
// and 120 req/s over 60s — same config, the ramp just fell in a different place.
//
// Leave at 0 unless you have measured your workload. Env: CRAFT_MIN_THREADS.
// (DOTNET_ThreadPool_MinThreads does NOT work — the host overwrites it at startup.)
// "MinThreads": 0,
// JSON files preloaded into PowerShell variables at startup
// "JsonPreloads": [{ "File": "Config/perms.json", "Variable": "Permissions", "Scope": "global", "AsHashtable": true }]
},
"Auth": {
// Session cookie name
"CookieName": "craft-session",
// Azure Table for user → role mappings (UPN as RowKey, Roles as JSON array)
"UserTableName": "allowedUsers"
// Storage connection for the allowedUsers table (empty = use AzureWebJobsStorage)
// "UserStorageConnection": "",
// Roles for the auto-login dev principal (Development mode only, ignored in Production)
// "DevRoles": ["admin", "authenticated", "anonymous"],
// Identity provider label on the auto-login dev principal's identityProvider field
// (Development mode only). Default: "aad". Set to "github", "google", etc. to mimic another
// EasyAuth provider for audit/display. Does NOT change the user-vs-API-client classification —
// the dev principal is always treated as an interactive user.
// "DevIdentityProvider": "aad",
// PowerShell function dispatched for /api/me (empty = literal "me")
// "MeEndpointFunction": "",
// Optional wrapper PS function for /api/me. When set, the wrapper is invoked
// instead of MeEndpointFunction directly, and MeEndpointFunction is passed
// in via Request.Params.CIPPEndpoint so the wrapper can dispatch internally.
// Empty (default) = invoke MeEndpointFunction directly.
// Example (CIPP): "New-CippCoreRequest"
// "MeEndpointHandler": ""
// ── Production auth is configured via environment variables or KV references: ──
// WEBSITE_AUTH_CLIENT_ID = Azure AD app registration client ID
// AUTH_SECRET = Azure AD client secret
// WEBSITE_AUTH_AAD_ALLOWED_TENANTS = Tenant ID (or "common" for multi-tenant)
},
// ── Storage backend (Azure Tables) ──
// The allowedUsers table (Auth) and orchestrator state resolve their connection as an explicit
// per-feature setting (e.g. Auth:UserStorageConnection) → the AzureWebJobsStorage env var. If
// neither is set the host FAILS TO START rather than silently falling back to the local emulator.
// "Storage": {
// // Allow the local storage emulator fallback when no connection string is configured.
// // Default false (fail closed). Also enabled by CRAFT_ALLOW_DEV_STORAGE=true or
// // ASPNETCORE_ENVIRONMENT=Development.
// "AllowDevelopmentStorage": false,
// // Max concurrent TCP connections to a single storage endpoint (Azure Tables provider). Azure
// // Table is HTTP/1.1 (no multiplexing), so this ceilings simultaneous in-flight requests and
// // stops a job fan-out exhausting the host's ~128 outbound-connection/SNAT budget on App Service.
// // Default 30 (matches the Function Apps setting). Set to 0 or a negative value for unlimited.
// // Env: CRAFT_STORAGE_MAX_CONNECTIONS_PER_SERVER.
// "MaxConnectionsPerServer": 30,
// // Minutes a pooled storage connection is reused before recycling (DNS/SNAT hygiene). Default 30.
// // Set to 0 or a negative value to disable recycling. Env: CRAFT_STORAGE_POOLED_CONNECTION_LIFETIME_MINUTES.
// "PooledConnectionLifetimeMinutes": 30
// },
// ── Native C# endpoints ──
// Endpoints written in C# rather than PowerShell, dispatched alongside them. Off unless you name
// the assemblies to scan, and costs nothing when off.
//
// NOT primarily a latency feature. Measured on one core at 40 req/s, a PowerShell endpoint that
// returns a constant costs 3.19ms median against 2.33ms for a C# one on the same host — so
// PowerShell dispatch is ~0.86ms and the rest is Kestrel, middleware and the container network,
// which this does not change. It is for the two things PowerShell cannot do: await (a PS endpoint
// holds a runspace AND a thread for every outbound call, which is why throughput follows
// HttpPoolSize / upstream_rtt), and stream (a PS endpoint returning a large result materialises it
// per concurrent caller — one measured 648 MiB of RSS at 8 concurrent).
//
// A native route is mapped at its literal path, and a literal route outranks the PowerShell
// catch-all, so endpoints migrate ONE AT A TIME with the PowerShell function still loaded as the
// rollback.
//
// The same assembly scan also finds:
// - ONE central handler (ICraftEndpointHandler) — the native counterpart of Scripts:HttpHandler.
// Every endpoint whose [CraftEndpoint] declares Dispatch=Central (the default) routes through
// it; endpoints that authenticate differently (webhooks) or serve anonymous callers declare
// Dispatch=Direct in code. Two handlers fail startup; zero is legal (all endpoints dispatch
// direct, the pre-handler behaviour).
// - Native scheduled tasks (ICraftScheduledTask + [CraftScheduledTask("Command")]) — fired by
// the scheduler from the SAME timer file as PowerShell commands. A timer Command resolves
// native-first, then falls back to the PS script table. They run on the .NET thread pool via
// the JobManager, so Worker:BgPoolSize=0 is a workable all-native configuration.
// "Endpoints": {
// "Enabled": false,
// // Absolute, or relative to the API base path.
// "Assemblies": ["bin/MyApp.Endpoints.dll"],
// // PreferNative (default) | PreferPowerShell | Fail
// // Fail is the safer setting for CI, where a route shadow should be caught before it ships.
// // PreferNative is the default because on a config-driven runtime a hard startup failure turns
// // an accidental shadow into a failed rolling deploy.
// "OnCollision": "PreferNative",
// // Refuse to start when Central-dispatch endpoints exist but no ICraftEndpointHandler was
// // found. Default false (a handler-less app simply dispatches everything direct). Set true —
// // in CI at minimum — when authorization lives in the handler: without it a missing handler
// // means every Central route serves with NO central check, and that should fail the deploy.
// "RequireHandler": false,
// // In-flight request limits. Distinct from RateLimit, which is per-client; these bound TOTAL
// // simultaneous work. Matters most with Worker:HttpPoolSize=0, where there is no runspace pool
// // to act as the implicit cap. 0 = unbounded.
// "MaxConcurrency": 0,
// // Per-route overrides, keyed by route. Overrides the endpoint's own declaration; set 0 to
// // remove a limit an endpoint declared.
// "Concurrency": { "MyBigDownload": 4 },
// // Seconds a request waits for a slot before being shed with 503. 0 = each endpoint's own.
// "QueueTimeoutSeconds": 0
// },
// "Setup": {
// "Enabled": false,
// "BootstrapClientId": "1950a258-227b-4e31-a9cf-717495945fc2",
// "AuthAppDisplayName": "",
// // RedirectToLoginPage | AllowAnonymous | RejectWith401 | RejectWith404
// "UnauthenticatedClientAction": "RedirectToLoginPage",
// // Paths excluded from EasyAuth (e.g. webhooks): ["/api/Public*", "/.auth/*"]
// "ExcludedPaths": [],
// // Client app IDs allowed to call with access tokens (empty = no restriction)
// "AllowedApplications": [],
// // Extra allowed token audiences beyond "api://{appId}" (empty = only api://{appId})
// "AllowedAudiences": [],
// // Tenant IDs allowed to auth. Empty = setup tenant only (single-tenant issuer).
// // Multiple entries → issuer becomes "common" + WEBSITE_AUTH_AAD_ALLOWED_TENANTS is set.
// "AllowedTenants": [],
// // Store EasyAuth client secret in Key Vault instead of app settings.
// // "auto" = use site name as vault name. Or set explicit vault name.
// // Empty (default) = store secret directly in AUTH_SECRET app setting.
// "KeyVaultName": "",
// // Key Vault secret names for the created SSO app registration's details, written
// // when KeyVaultName is set. Defaults match the names CIPP expects; override to change.
// // AppSecret is also the SecretName the AUTH_SECRET app setting references.
// "SsoSecretNames": { "AppSecret": "SSOAppSecret", "AppId": "SSOAppId", "MultiTenant": "SSOMultiTenant" },
// // Roles assigned to the bootstrap user seeded via /api/setup/seed-user.
// // Empty (default) = ["superadmin"]. Override to match the hosted app's role taxonomy.
// "FirstUserRoles": []
// },
// OAuth discovery documents served by Craft from app settings, verbatim — the hosted app
// controls every field. Two documents:
// /.well-known/oauth-protected-resource (RFC 9728, from SettingName, default CRAFT_PRM):
// how MCP/OAuth clients discover the authorization server and scopes for the hosted API.
// /.well-known/oauth-authorization-server + /.well-known/openid-configuration (RFC 8414,
// from AuthServerSettingName, default CRAFT_PRM_AS, optional): an authorization server
// document the app authors itself — typically mirroring Entra's endpoints verbatim while
// adding a registration_endpoint (dynamic client registration, which Entra lacks). Craft
// serves the JSON only; authorize/token stay at Entra, registration is a hosted-app route.
// A literal {origin} anywhere in the JSON is replaced per-request with https://{host}
// (RFC 9728 requires `resource` to equal the URL the client connects to — only the request
// knows it once custom domains exist). There is NO separate enable switch: presence of the
// app settings IS the switch — the hosted app writes them to turn discovery on and clears
// them to turn it off; the setup reconcile auto-appends/removes the well-known paths in
// EasyAuth's ExcludedPaths accordingly (app setting changes restart the container, so both
// converge on the same boot). Exists because the platform's own PRM (preview) pins
// authorization_servers to the SSO issuer (multi-tenant SSO advertises /common, which a
// single-tenant resource app rejects with AADSTS50194). Leave
// WEBSITE_AUTH_PRM_DEFAULT_WITH_SCOPES UNSET — that setting activates the platform document,
// which intercepts the path before Craft sees it.
// "Prm": {
// "SettingName": "CRAFT_PRM",
// "AuthServerSettingName": "CRAFT_PRM_AS"
// },
"Scheduler": {
// JSON file with task definitions (looked up in API/Config/ then API/)
"ConfigFile": "SchedulerTasks.json",
// How often (seconds) the scheduler checks for due tasks
"CheckIntervalSeconds": 30,
// When true, applies the configured timezone to ALL tasks (not just TZOffset=true ones)
"ApplyTZOffset": false,
// IANA or Windows timezone ID for timezone-aware cron evaluation.
// Overridden by env var CraftTZ if set. Empty = UTC.
// Examples: "America/New_York", "Europe/London"
"Timezone": ""
},
"Orchestrator": {
// Prefix for Azure Tables: {Prefix}Runs, {Prefix}Tasks, {Prefix}Results
"TablePrefix": "Orchestrator",
// Max task interruptions (crash/restart) before marking Failed
"MaxRetries": 3,
// Retention sweep over the three tables: runs that finished (or, if nothing is driving them, last
// wrote to storage) longer ago than RetentionHours go, as do Tasks/Results partitions whose Run row
// is gone. Once at startup after crash recovery, then every CleanupIntervalHours (0 = startup only).
"RetentionHours": 48,
"CleanupIntervalHours": 4,
// Batch + coalesce per-task/run status writes off the fan-out critical path (results are never batched).
// Default true. Removes the per-task Azure Table write that gates worker throughput — see
// docs/orch-analysis.md. Set false for the original per-task writes.
// "BatchStatusWrites": true,
// Write the pre-invoke "Running" marker under a durable barrier (persisted before the task runs, so
// AttemptCount/MaxRetries still bounds poison tasks). Default true. False = eventual (max throughput —
// 100% pool utilization — but the marker rides the periodic flush; terminal/run states stay durable).
// "DurableRunningBarrier": true,
// Status writer flush interval / barrier latency ceiling (ms). Default 25.
// "StatusFlushIntervalMs": 25,
// How long a task waits for its durable "Running" marker before deferring (seconds). Default 90.
// This wait sits between dispatch and worker checkout while holding a JobManager slot, so leaving it
// unbounded turns a stalled flush into a host-wide outage. On timeout the task is deferred and
// retried, never failed — the marker did not land, so storage still has it Pending.
// Must exceed StatusFlushTimeoutSeconds.
// "RunningBarrierTimeoutSeconds": 90,
// Ceiling on a single flush of coalesced status writes (seconds). Default 30. The drain loop is one
// loop: an unbounded storage call inside a flush stops every status write in the process. Writes
// that do not complete are requeued and retried, so a timeout never loses one.
// "StatusFlushTimeoutSeconds": 30,
// Per-run status writes in flight within one flush. Default 8. Writes group by run (a batch must
// share a partition key), so a workload of many single-task runs — e.g. one run per tenant — would
// otherwise serialize into hundreds of round-trips per flush. 1 = original sequential behaviour.
// "StatusFlushConcurrency": 8
},
"Cache": {
// Whether the in-memory/disk-backed API response cache is active. Omit (default) for auto: ON only
// when this node serves BOTH a browser UI and its API (combined / frontend+http roles), OFF for
// api-only, worker-only and static-only nodes. Set true/false to force it. Env override (wins):
// CRAFT_RESPONSE_CACHE=true/false. When off, no _cache/ dir is created and get/set are no-ops.
// "Enabled": true,
// Bytes budget for keeping cached response bodies in memory (LRU tier over the disk cache) so a HIT
// returns from RAM instead of re-reading the file. Default 64 MiB; 0 = disk-only. Big win for large
// List* responses (+157% throughput at ~150KB — see docs/cache-analysis.md).
// "MaxMemoryBytes": 67108864,
// Maximum cached responses held in memory
"MaxEntries": 1000,
// Default TTL (seconds) for cached responses
"DefaultTtlSeconds": 600
// Query parameter that triggers cache invalidation when "true"
// "InvalidateParam": "InvalidateCache",
// Query parameter for scoped invalidation (e.g. per-tenant)
// "ScopeParam": "",
// Endpoints never cached, whatever the query string says. Case-insensitive; "*" matches any run of
// characters. For reads the param rules can't classify — log tails, scheduler views, anything whose
// result depends on who is asking. e.g. [ "ListLogs", "ListScheduled*" ]
// "ExcludedEndpoints": [],
// Query parameter a request must carry before its response may be cached. Empty (default) = cache
// every eligible read. Set it to the param that makes a response distinct (CIPP: "tenantFilter")
// so endpoints that don't take it — ListTenants, log tails, the scheduler view — are never cached.
// "RequiredParam": "",
// Values of RequiredParam that skip the cache (case-insensitive). Only meaningful with RequiredParam.
// e.g. [ "AllTenants" ] — an all-tenant result is a different shape from any single-tenant one.
// "ExcludedParamValues": [],
// Request header that bypasses the cache for a single call (no read, no write). Empty = no header
// check. Any non-empty value bypasses except an explicit "false"/"0"/"no".
// "NoCacheHeader": "x-craft-no-cache",
// Per-endpoint TTL overrides (seconds): { "ListUsers": 120 }
// "EndpointTtl": {}
},
"Scripts": {
// Module directories (under API/Modules/) scanned for HTTP endpoint functions
// "HttpModules": [],
// Optional global HTTP handler. When set, ALL /API/{endpoint} routes are dispatched
// through this single PS function instead of invoking the route's function directly.
// The endpoint name is passed via Request.Params.CIPPEndpoint so the handler can
// dispatch internally. Use when the hosted app expects all routes to go through a
// common router (e.g. CIPP's New-CippCoreRequest which does Test-CIPPAccess + telemetry).
// Empty (default) = each /API/{endpoint} invokes Invoke-{endpoint} directly.
// "HttpHandler": "New-CippCoreRequest",
// Directories (under API/) scanned for background scripts
// "BackgroundScriptDirs": [],
// RBAC metadata extraction from .ROLE / .FUNCTIONALITY comment-based help
"PermissionExtraction": {
"Enabled": false
// "Modules": [],
// "OutputFile": "function-permissions.json"
}
}
// Container health monitoring — detects crash loops and forces Azure to
// provision a new worker by refusing to start Kestrel after repeated failures.
// Tracker file is written to /home/craft/ (Azure Files persistent mount).
// "ContainerHealth": {
// "MaxRestarts": 3, // consecutive crashes before blocking startup (0 = disabled)
// "WindowMinutes": 30, // only count restarts within this window
// "TrackerDirectory": "" // default: /home/craft (Linux) or disabled (Windows/dev)
// }
// Frontend response policy. EasyAuth handles auth/redirects at the platform layer;
// CRAFT only adds response headers EasyAuth doesn't touch.
// "Frontend": {
// // Content-Security-Policy applied to all responses. DEFAULTS to the CIPP-compatible policy
// // shown below — a CSP is emitted secure-by-default even if you don't set this. Override to
// // tighten it, or set to "" to disable. Note: HSTS / X-Content-Type-Options / X-Frame-Options
// // are intentionally NOT emitted here — they are left to the edge (Azure front end) / hosted app.
// // Keep 'self' in default-src AND connect-src if you override this: it is what allows the app's
// // own same-origin fetches when it is reached over http (TLS-terminating proxy, self-hosted,
// // local docker). Dropping it leaves the policy scheme-gated on https: and every same-origin
// // XHR is blocked. Keep data: in connect-src too: wasm inlined as a data: URL (Emscripten
// // SINGLE_FILE, e.g. yoga-layout under @react-pdf/renderer) is fetched, not scripted, so it is
// // connect-src that gates it — and keeping it out of default-src keeps it out of script-src.
// "ContentSecurityPolicy": "default-src 'self' https: blob: 'unsafe-eval' 'unsafe-inline'; connect-src 'self' https: blob: data:; object-src 'self' blob:; img-src 'self' blob: data: *",
// // Compress static responses. Default true: serve precompressed .br/.gz siblings + on-the-fly
// // fallback. Set false to serve everything raw/identity (no .br/.gz served, no on-the-fly) — e.g.
// // when an upstream CDN (Cloudflare) already compresses, the content doesn't benefit, or for
// // A/B measuring compressed vs raw. Env override: CRAFT_COMPRESSION=true|false (wins over this).
// "Compression": true
// }
// Dynamic /api response policy — independent of the static Frontend block above.
// "Api": {
// // Compress /api (PowerShell / native dispatch) responses on the fly: Brotli preferred, gzip
// // fallback, negotiated from the caller's Accept-Encoding. Default true, and INDEPENDENT of
// // Frontend.Compression — an origin behind a CDN that compresses the static bundle typically does
// // not re-compress API JSON, so the origin keeps doing it even with static compression off. A
// // response is only compressed when the caller advertises an accepted encoding; otherwise it is
// // served identity. Env override: CRAFT_API_COMPRESSION=true|false (wins over this).
// "Compression": true,
// // On-the-fly compression level for the Brotli/gzip providers: Fastest | Optimal | SmallestSize
// // | NoCompression. Default Optimal — measured on 2 vCPU, Brotli Optimal ~8.4x vs Fastest ~4.9x at
// // the SAME CPU (~14%) and latency, so it is a near-free win. Do NOT use SmallestSize for /api:
// // Brotli q11 pegs both cores (~180% CPU) and drives p95 into the tens of seconds. Drop to Fastest
// // (or NoCompression) on a very small SKU if the compressor competes with the PowerShell pool.
// // Applies to on-the-fly compression (dynamic /api + the static fallback for sibling-less assets);
// // precompressed .br/.gz siblings are unaffected. Env override: CRAFT_API_COMPRESSION_LEVEL.
// "CompressionLevel": "Optimal"
// }
}
}