@o3co/create-auth-policy-verifier 0.3.1 → 0.4.0
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/README.ja.md +27 -7
- package/README.md +27 -7
- package/dist/cli.mjs +0 -0
- package/dist/index.d.mts +34 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +110 -12
- package/package.json +40 -30
- package/templates/standalone/Dockerfile +75 -11
- package/templates/standalone/README.ja.md +245 -12
- package/templates/standalone/README.md +248 -12
- package/templates/standalone/config/application.conf +300 -4
- package/templates/standalone/config/development.conf +8 -0
- package/templates/standalone/config/production.conf +9 -0
- package/templates/standalone/docker-compose.yml +7 -0
- package/templates/standalone/package.json +11 -9
- package/templates/standalone/src/__tests__/configPath.test.mts +37 -0
- package/templates/standalone/src/__tests__/loadConfig.test.mts +414 -0
- package/templates/standalone/src/__tests__/logger.test.mts +56 -0
- package/templates/standalone/src/__tests__/smoke.test.mts +254 -48
- package/templates/standalone/src/configPath.ts +21 -0
- package/templates/standalone/src/loadConfig.ts +22 -0
- package/templates/standalone/src/logger.ts +33 -0
- package/templates/standalone/src/main.mts +11 -14
- package/templates/standalone/tsconfig.json +2 -1
- package/templates/standalone/tsconfig.typecheck.json +8 -0
- package/templates/versions.json +3 -3
- package/dist/__tests__/index.test.d.mts +0 -2
- package/dist/__tests__/index.test.d.mts.map +0 -1
- package/dist/__tests__/index.test.mjs +0 -213
|
@@ -1,35 +1,331 @@
|
|
|
1
1
|
http {
|
|
2
|
-
|
|
2
|
+
# Loopback by default: /verify answers with an authorization decision, so a
|
|
3
|
+
# reachable port is a decision oracle for anyone who can route to it. The
|
|
4
|
+
# deployment shape this template targets is a sidecar — the enforcement layer
|
|
5
|
+
# talks to the verifier over loopback and nothing else can.
|
|
6
|
+
#
|
|
7
|
+
# A containerised deployment must bind all interfaces to be reachable from
|
|
8
|
+
# another container: set HTTP_HOSTNAME=0.0.0.0 explicitly (docker-compose.yml
|
|
9
|
+
# does), and configure http.callerAuth below so the reachable port is not open.
|
|
10
|
+
hostname = "127.0.0.1"
|
|
3
11
|
hostname = ${?HTTP_HOSTNAME}
|
|
12
|
+
# A whole number from 1 to 65535. Like every numeric setting in this file it
|
|
13
|
+
# is checked at boot, so a variable exported empty or holding something that
|
|
14
|
+
# is not a number fails here rather than binding an arbitrary free port.
|
|
4
15
|
port = 3000
|
|
5
16
|
port = ${?HTTP_PORT}
|
|
6
17
|
pathPrefix = ""
|
|
7
18
|
pathPrefix = ${?HTTP_PATH_PREFIX}
|
|
19
|
+
|
|
20
|
+
# Optional shared credential authenticating the SERVICE that calls /verify —
|
|
21
|
+
# a different question from the bearer token, which only establishes the
|
|
22
|
+
# subject a decision is about. Set HTTP_CALLER_AUTH_TOKEN and have the
|
|
23
|
+
# enforcement layer send the same value in the header below; the verifier then
|
|
24
|
+
# rejects every other caller before it does any decision work.
|
|
25
|
+
#
|
|
26
|
+
# Leaving the token unset leaves the endpoints open to anyone who can reach
|
|
27
|
+
# the port. That is supported (and is fine on loopback), but the server logs
|
|
28
|
+
# `unauthenticated_non_loopback_bind` at boot if the bind is not loopback.
|
|
29
|
+
callerAuth {
|
|
30
|
+
token = ${?HTTP_CALLER_AUTH_TOKEN}
|
|
31
|
+
# Not `Authorization` — that header carries the subject token.
|
|
32
|
+
header = "x-caller-token"
|
|
33
|
+
header = ${?HTTP_CALLER_AUTH_HEADER}
|
|
34
|
+
}
|
|
8
35
|
}
|
|
9
36
|
|
|
10
37
|
oauth {
|
|
11
38
|
jwt {
|
|
12
39
|
algorithm = "HS256"
|
|
13
40
|
algorithm = ${?OAUTH_JWT_ALGORITHM}
|
|
41
|
+
# The HS256 shared secret. It must carry at least 32 bytes (256 bits) of
|
|
42
|
+
# key material or the process refuses to boot: HS256 is symmetric, so the
|
|
43
|
+
# value that verifies a token is the value that signs one, and guessing it
|
|
44
|
+
# buys the ability to MINT tokens for any subject. Generate it with
|
|
45
|
+
# `openssl rand -hex 32` (or -base64 32) and never write it in this file.
|
|
46
|
+
#
|
|
47
|
+
# The floor is measured on DECODED material at the smallest plausible
|
|
48
|
+
# reading: `openssl rand -hex 16` is 32 characters but only 16 bytes and is
|
|
49
|
+
# refused, and 32 random alphanumerics read as a 24-byte base64 body. The
|
|
50
|
+
# same floor applies to every previousSecrets entry below.
|
|
14
51
|
secret = ${?OAUTH_JWT_SECRET}
|
|
52
|
+
# HS256 only. Names the secret above, matching the `kid` auth.provider
|
|
53
|
+
# stamps into every token it mints (its own default is "v0"). Leave it unset
|
|
54
|
+
# and the token header is never consulted — one secret verifies everything,
|
|
55
|
+
# which is the shape a deployment that has never rotated wants. Set it and a
|
|
56
|
+
# token carrying a different kid is refused, which is what makes the
|
|
57
|
+
# rotation block below unambiguous. Harmless under RS256/ES256/EdDSA, which
|
|
58
|
+
# match kid against the JWKS they fetch and ignore this key.
|
|
59
|
+
kid = ${?OAUTH_JWT_KID}
|
|
60
|
+
# HS256 only, and unlike `kid` this one is REFUSED at boot under
|
|
61
|
+
# RS256/ES256/EdDSA — an empty list included. Those rotate through the JWKS
|
|
62
|
+
# at jwksUri, which already carries every key the issuer publishes, so a
|
|
63
|
+
# previousSecrets block left behind on an asymmetric config configures
|
|
64
|
+
# nothing and is rejected rather than silently dropped.
|
|
65
|
+
#
|
|
66
|
+
# Secrets a rotation has retired but not finished retiring. Without them,
|
|
67
|
+
# rotating an HS256 secret is a coordinated outage: the moment the provider
|
|
68
|
+
# signs with a new value, every token already in flight is refused until
|
|
69
|
+
# both services have restarted together.
|
|
70
|
+
#
|
|
71
|
+
# Not declared here, and deliberately: nothing is being rotated yet, and an
|
|
72
|
+
# empty anchor in this shared file is exactly what would break a deployment
|
|
73
|
+
# that switches OAUTH_JWT_ALGORITHM to an asymmetric one. Add it to your own
|
|
74
|
+
# overlay while a rotation is open, then delete it once the window closed:
|
|
75
|
+
#
|
|
76
|
+
# oauth.jwt {
|
|
77
|
+
# kid = "v1" # the secret the provider signs with now
|
|
78
|
+
# secret = ${?OAUTH_JWT_SECRET}
|
|
79
|
+
# previousSecrets = [{
|
|
80
|
+
# kid = "v0" # the secret it signed with before
|
|
81
|
+
# secret = ${?OAUTH_JWT_PREVIOUS_SECRET}
|
|
82
|
+
# expiresAt = "2026-09-01T00:00:00Z" # access-token TTL + buffer
|
|
83
|
+
# }]
|
|
84
|
+
# }
|
|
85
|
+
#
|
|
86
|
+
# kid + secret + expiresAt is auth.provider's own previousSecrets shape, so
|
|
87
|
+
# the same pair of values moves on both sides. A token that carries no kid
|
|
88
|
+
# at all is tried against every secret listed here, which is why the list is
|
|
89
|
+
# capped at 3 entries — its length is the work one request can cost.
|
|
90
|
+
# Entries past their expiresAt stop verifying without a restart.
|
|
91
|
+
# JWKS endpoint for RS256/ES256/EdDSA. Must be https, because every key this
|
|
92
|
+
# endpoint hands back can verify tokens the service accepts: over plaintext,
|
|
93
|
+
# anyone on the network path swaps in their own signing key and forges tokens
|
|
94
|
+
# that pass. TLS is what proves the endpoint is the one you meant.
|
|
95
|
+
# Plaintext http is accepted only for loopback hosts (localhost, 127.0.0.0/8,
|
|
96
|
+
# [::1]), where no network path exists to attack — a service reached by
|
|
97
|
+
# container or DNS name is NOT loopback. Anything else fails at boot.
|
|
15
98
|
jwksUri = ${?OAUTH_JWT_JWKS_URI}
|
|
99
|
+
# Bounds on the JWKS fetch, which happens inside a verify request whenever
|
|
100
|
+
# key resolution misses the cache. Leave them alone unless the provider's
|
|
101
|
+
# latency or rotation cadence demands otherwise.
|
|
102
|
+
jwksTimeoutMs = 5000
|
|
103
|
+
jwksTimeoutMs = ${?OAUTH_JWT_JWKS_TIMEOUT_MS}
|
|
104
|
+
# Minimum spacing between fetches. A token carrying an unknown kid triggers a
|
|
105
|
+
# refetch and kid is attacker-controlled, so this is also the throttle that
|
|
106
|
+
# keeps forged headers from becoming a fetch storm at the provider.
|
|
107
|
+
jwksCooldownMs = 30000
|
|
108
|
+
jwksCooldownMs = ${?OAUTH_JWT_JWKS_COOLDOWN_MS}
|
|
109
|
+
jwksCacheMaxAgeMs = 600000
|
|
110
|
+
jwksCacheMaxAgeMs = ${?OAUTH_JWT_JWKS_CACHE_MAX_AGE_MS}
|
|
16
111
|
publicKey = ${?OAUTH_JWT_PUBLIC_KEY}
|
|
17
112
|
publicKeyPath = ${?OAUTH_JWT_PUBLIC_KEY_PATH}
|
|
18
|
-
|
|
19
|
-
|
|
113
|
+
|
|
114
|
+
# RFC 9068 §4 — required whenever mode = "verify". No default: booting
|
|
115
|
+
# without them fails rather than accepting any validly-signed token.
|
|
116
|
+
# A single issuer, or a list in a config file when several are acceptable.
|
|
117
|
+
issuer = ${?OAUTH_JWT_ISSUER}
|
|
118
|
+
audience = ${?OAUTH_JWT_AUDIENCE}
|
|
119
|
+
# Accepted `typ` header. at+jwt is the RFC 9068 access-token type; pinning it
|
|
120
|
+
# rejects id_tokens, refresh tokens and logout tokens signed with the same key.
|
|
121
|
+
tokenType = "at+jwt"
|
|
122
|
+
tokenType = ${?OAUTH_JWT_TOKEN_TYPE}
|
|
123
|
+
|
|
124
|
+
# Bounds on a presented token's own lifetime. Both apply in EVERY mode,
|
|
125
|
+
# insecure-decode included. `exp` itself is required and has no setting:
|
|
126
|
+
# a token that never states when it stops being valid never stops.
|
|
127
|
+
#
|
|
128
|
+
# maxTokenAgeSeconds is the ceiling on how long after issuance (`iat`) a
|
|
129
|
+
# token may still be presented, whatever `exp` its issuer chose — the
|
|
130
|
+
# backstop against a decade-long access token. Setting it makes `iat`
|
|
131
|
+
# required, which RFC 9068 §2.2 requires of an access token anyway. A day
|
|
132
|
+
# is deliberately loose; lower it to match how long your tokens really
|
|
133
|
+
# live, and raise it only for an issuer that mints genuinely long ones.
|
|
134
|
+
maxTokenAgeSeconds = 86400
|
|
135
|
+
maxTokenAgeSeconds = ${?OAUTH_JWT_MAX_TOKEN_AGE_SECONDS}
|
|
136
|
+
# Skew allowance on every time-claim comparison (exp, nbf, and the age
|
|
137
|
+
# ceiling). 0 means the verifier trusts its clock and the issuer's to agree,
|
|
138
|
+
# which is right for a sidecar sharing a host. Where they are separate
|
|
139
|
+
# machines, 60 matches the skew the paired provider allows. Capped at 300:
|
|
140
|
+
# tolerance lengthens the accepted life of every token, so an unbounded
|
|
141
|
+
# value is a way to spell "expiry optional" without writing it down.
|
|
142
|
+
clockToleranceSeconds = 0
|
|
143
|
+
clockToleranceSeconds = ${?OAUTH_JWT_CLOCK_TOLERANCE_SECONDS}
|
|
144
|
+
|
|
145
|
+
# How bearer tokens are treated. "verify" (the default) fully verifies
|
|
146
|
+
# signature, iss, aud and the typ header. "insecure-decode" is TEST-ONLY:
|
|
147
|
+
# it accepts tokens without signature verification (exp/nbf are still
|
|
148
|
+
# enforced). The value itself is the consent — a mistyped env var can flip
|
|
149
|
+
# a boolean, but it cannot produce the literal string "insecure-decode".
|
|
150
|
+
# Never set insecure-decode in production.
|
|
151
|
+
mode = "verify"
|
|
152
|
+
mode = ${?OAUTH_JWT_MODE}
|
|
20
153
|
}
|
|
21
154
|
}
|
|
22
155
|
|
|
156
|
+
logging {
|
|
157
|
+
# Minimum level the process emits: trace/debug/info/warn/error/fatal, or
|
|
158
|
+
# silent to drop everything. Failure events the operator alerts on
|
|
159
|
+
# (jwt_verification_unavailable, verify_internal_error) emit at error.
|
|
160
|
+
#
|
|
161
|
+
# This is also the switch for the per-decision audit log. One `decision`
|
|
162
|
+
# event is emitted for every allow AND every deny, at info, carrying sub,
|
|
163
|
+
# resource, action, the rule that decided, the request id and the latency —
|
|
164
|
+
# so raising this to warn turns that stream off wholesale. It is deliberately
|
|
165
|
+
# not at warn: a deny is a normal outcome for a decision point, and routing
|
|
166
|
+
# it there would let any caller manufacture warn-level noise. The bearer
|
|
167
|
+
# token, the claims beyond sub, and the caller's `context` object are never
|
|
168
|
+
# on the line. See the README's Observability section.
|
|
169
|
+
level = "info"
|
|
170
|
+
level = ${?LOG_LEVEL}
|
|
171
|
+
}
|
|
172
|
+
|
|
23
173
|
attribute {
|
|
174
|
+
# What a request is described by. Every rule below decides from these
|
|
175
|
+
# attributes and nothing else, so a rule whose attribute nothing here produces
|
|
176
|
+
# can never pass — see the note in the rule block.
|
|
24
177
|
collectors = [
|
|
178
|
+
# The token's `scope` claim, split into the list the scope rule reads.
|
|
25
179
|
{ collector = "PayloadScopeCollector" }
|
|
180
|
+
# The token's `sub`, which becomes the `subject` on the decision response
|
|
181
|
+
# and the `sub` field on the audit log line.
|
|
26
182
|
{ collector = "PayloadSubjectIdCollector" }
|
|
27
183
|
]
|
|
184
|
+
# Deliberately absent: anything supplying permissions or roles. This
|
|
185
|
+
# deployment authorizes from the token's scopes alone. Adding a permission
|
|
186
|
+
# rule means adding its supplier here in the same edit (#113).
|
|
28
187
|
}
|
|
29
188
|
|
|
30
189
|
rule {
|
|
190
|
+
# Decision for a request that collects no rule at all. "deny" (the default)
|
|
191
|
+
# keeps the verifier fail-closed; "allow" is an explicit opt-out and makes
|
|
192
|
+
# any request that matches no rule succeed.
|
|
193
|
+
onEmptyRuleSet = "deny"
|
|
194
|
+
onEmptyRuleSet = ${?RULE_ON_EMPTY_RULE_SET}
|
|
195
|
+
|
|
196
|
+
# The shipped policy is the token's own `scope` claim and nothing else: the
|
|
197
|
+
# request is allowed exactly when the token carries "<action>:<resourceType>".
|
|
198
|
+
# It works against any issuer that mints scopes without an authorization store
|
|
199
|
+
# to stand up first, and it stays fail-closed — a token carrying no scope, or
|
|
200
|
+
# the wrong one, is denied, as is a request that collects no rule at all.
|
|
201
|
+
#
|
|
202
|
+
# Rules are grouped by kind and every GROUP must pass, so one group nothing can
|
|
203
|
+
# satisfy denies every request the deployment ever sees. That is what enabling
|
|
204
|
+
# a rule collector without the attribute collector feeding it does, and it is
|
|
205
|
+
# why the permission rule below is off rather than merely unconfigured (#113).
|
|
31
206
|
collectors = [
|
|
207
|
+
# Scope matching is exact and case-sensitive (RFC 6749 §3.3): the token must
|
|
208
|
+
# carry "<action>:<resourceType>" verbatim. If your issuer emits bare resource
|
|
209
|
+
# names ("document") instead ("read:document"), opt into the rewrite
|
|
210
|
+
# explicitly — it is off by default because it invents an action the issuer
|
|
211
|
+
# never wrote:
|
|
212
|
+
# { collector = "ResourceActionScopeRuleCollector", allowBareScopeRewrite = true }
|
|
32
213
|
{ collector = "ResourceActionScopeRuleCollector" }
|
|
33
|
-
|
|
214
|
+
|
|
215
|
+
# NOT enabled, and not an oversight. ResourceActionPermissionRuleCollector
|
|
216
|
+
# emits a rule requiring the permission "<resource.raw>.perm:<action>" —
|
|
217
|
+
# "project:1.perm:read" for resource "project:1", action "read" — and that
|
|
218
|
+
# rule reads the permissions/roles attributes, which nothing in the
|
|
219
|
+
# `attribute.collectors` above produces. Enabled on its own it is a group no
|
|
220
|
+
# token can satisfy, i.e. a verifier that answers deny to everything.
|
|
221
|
+
#
|
|
222
|
+
# So enable it together with a supplier, in one edit — two list entries that
|
|
223
|
+
# are one decision. Add this entry to `attribute.collectors` above:
|
|
224
|
+
#
|
|
225
|
+
# { collector = "StaticPermissionCollector", permissions = ["project:1.perm:read"] }
|
|
226
|
+
#
|
|
227
|
+
# and this entry to the list you are reading now:
|
|
228
|
+
#
|
|
229
|
+
# { collector = "ResourceActionPermissionRuleCollector" }
|
|
230
|
+
#
|
|
231
|
+
# StaticPermissionCollector grants one fixed list to every subject, so it
|
|
232
|
+
# suits a per-deployment capability, never a per-user one. Where permissions
|
|
233
|
+
# differ by subject they live in your own store: write an AttributeCollector
|
|
234
|
+
# that reads it and register it in a module (README.md, "Adding Custom
|
|
235
|
+
# Modules", and docs/extending.md).
|
|
34
236
|
]
|
|
35
237
|
}
|
|
238
|
+
|
|
239
|
+
resource {
|
|
240
|
+
# How the request's `resource` string is turned into the resourceType that the
|
|
241
|
+
# scope rules above authorize. DotNotationResourceParser reads
|
|
242
|
+
# `type[:id]` segments separated by ".", and joins the segment types with "."
|
|
243
|
+
# — "project:1.member:2" is type "project.member", id "2". The separator is
|
|
244
|
+
# kept rather than rewritten to "_", so the nested type "a.b" and a flat type
|
|
245
|
+
# literally named "a_b" are two different namespaces.
|
|
246
|
+
#
|
|
247
|
+
# Strings outside that grammar are answered 400 invalid_request, never
|
|
248
|
+
# repaired: an empty segment ("a..b"), a second ":" in one segment ("a:1:2",
|
|
249
|
+
# which is refused rather than read as "a:1"), whitespace (" a:1 " is not
|
|
250
|
+
# trimmed), and any character an OAuth scope value cannot carry. If your
|
|
251
|
+
# callers send resource strings in some other syntax, register your own
|
|
252
|
+
# ResourceParser rather than relying on this one to interpret them.
|
|
253
|
+
parser = "DotNotationResourceParser"
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
verify {
|
|
257
|
+
# Cap on `POST /verify/batch` entries. The batch endpoint exists so filtering
|
|
258
|
+
# a list of N resources costs one round trip instead of N; the cap keeps one
|
|
259
|
+
# request from turning into an unbounded amount of pipeline work.
|
|
260
|
+
maxBatchSize = 50
|
|
261
|
+
maxBatchSize = ${?VERIFY_MAX_BATCH_SIZE}
|
|
262
|
+
|
|
263
|
+
# Ceiling on the JSON body, in bytes — the limit handed to `express.json()`.
|
|
264
|
+
# Below Express's unstated 100 KB default, and the outer envelope: it is what
|
|
265
|
+
# binds first on a large batch, since the per-field limits below bound ONE
|
|
266
|
+
# entry rather than N of them. A body over it is answered
|
|
267
|
+
# `413 payload_too_large` in the same deny envelope as everything else.
|
|
268
|
+
maxBodyBytes = 65536
|
|
269
|
+
maxBodyBytes = ${?VERIFY_MAX_BODY_BYTES}
|
|
270
|
+
|
|
271
|
+
# Ceilings on the two identifiers, in characters. Whitespace in either is
|
|
272
|
+
# refused rather than trimmed — they are concatenated into the
|
|
273
|
+
# `{action}:{resourceType}` scope an issuer has to have granted, and RFC 6749
|
|
274
|
+
# §3.3 makes space the delimiter between scope values.
|
|
275
|
+
maxResourceLength = 512
|
|
276
|
+
maxResourceLength = ${?VERIFY_MAX_RESOURCE_LENGTH}
|
|
277
|
+
maxActionLength = 64
|
|
278
|
+
maxActionLength = ${?VERIFY_MAX_ACTION_LENGTH}
|
|
279
|
+
|
|
280
|
+
# Bounds on the request `context`, which is free-form caller-supplied data.
|
|
281
|
+
# `maxContextEntries` counts every property and every array element in the
|
|
282
|
+
# whole tree, at every depth — nesting is bounded rather than forbidden, since
|
|
283
|
+
# RequestContextAttributeCollector reads dot paths such as "tenant.id" — and
|
|
284
|
+
# because each level of nesting costs at least one entry, it bounds the depth
|
|
285
|
+
# too. `maxContextValueLength` bounds every string in that tree, property
|
|
286
|
+
# names included.
|
|
287
|
+
maxContextEntries = 64
|
|
288
|
+
maxContextEntries = ${?VERIFY_MAX_CONTEXT_ENTRIES}
|
|
289
|
+
maxContextValueLength = 1024
|
|
290
|
+
maxContextValueLength = ${?VERIFY_MAX_CONTEXT_VALUE_LENGTH}
|
|
291
|
+
|
|
292
|
+
# Bounds on the collector fan-out both pipelines run for every decision.
|
|
293
|
+
# Collectors are the layer that calls databases and HTTP APIs, so they are the
|
|
294
|
+
# layer that can stall; without these, one collector holding a dead socket
|
|
295
|
+
# holds the whole decision.
|
|
296
|
+
#
|
|
297
|
+
# EXCEEDING ANY OF THEM DENIES the decision — `403 collector_timeout`, with an
|
|
298
|
+
# empty `reason.groups`. It is never answered with whatever was collected in
|
|
299
|
+
# time: fewer rules is a weaker policy, and no rules at all is an ALLOW
|
|
300
|
+
# wherever `rule.onEmptyRuleSet = "allow"` is set.
|
|
301
|
+
#
|
|
302
|
+
# `collectorTimeoutMs` is one collector's own budget, counted from when that
|
|
303
|
+
# collector starts rather than from when the fan-out did, so waiting behind
|
|
304
|
+
# the concurrency cap does not spend it.
|
|
305
|
+
collectorTimeoutMs = 2000
|
|
306
|
+
collectorTimeoutMs = ${?VERIFY_COLLECTOR_TIMEOUT_MS}
|
|
307
|
+
|
|
308
|
+
# `collectorDeadlineMs` is the whole fan-out, per pipeline. The per-collector
|
|
309
|
+
# budget cannot bound a SET: once collectors queue, enough of them each
|
|
310
|
+
# finishing just inside their own budget still adds up to a request nobody is
|
|
311
|
+
# waiting for any more.
|
|
312
|
+
collectorDeadlineMs = 5000
|
|
313
|
+
collectorDeadlineMs = ${?VERIFY_COLLECTOR_DEADLINE_MS}
|
|
314
|
+
|
|
315
|
+
# `collectorConcurrency` is how many run at once, per pipeline, per decision.
|
|
316
|
+
# Higher than any realistic collector set, so an ordinary configuration still
|
|
317
|
+
# fans out in one wave; what it removes is the tail, where a wide config or a
|
|
318
|
+
# full batch multiplies into simultaneous calls against a dependency that has
|
|
319
|
+
# just started to slow down.
|
|
320
|
+
collectorConcurrency = 8
|
|
321
|
+
collectorConcurrency = ${?VERIFY_COLLECTOR_CONCURRENCY}
|
|
322
|
+
|
|
323
|
+
# Whether collectors receive the raw credential as `context.credential`
|
|
324
|
+
# (#175). "never" (the default): collectors get verified claims only — the
|
|
325
|
+
# credential is replayable, and a collector that logs its context would leak
|
|
326
|
+
# a live token. Set "expose" ONLY when a project-side collector calls a
|
|
327
|
+
# downstream API as the subject (token forwarding / exchange); this line is
|
|
328
|
+
# then the greppable record of that decision. Never log the field.
|
|
329
|
+
credentialToCollectors = "never"
|
|
330
|
+
credentialToCollectors = ${?VERIFY_CREDENTIAL_TO_COLLECTORS}
|
|
331
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Environment overlay for development.
|
|
2
|
+
# Loaded when CONFIG_ENV (or NODE_ENV) is "development" — the default.
|
|
3
|
+
# Values here override application.conf.
|
|
4
|
+
#
|
|
5
|
+
# Most values are controlled via environment variables (see ${?VAR}
|
|
6
|
+
# substitutions in application.conf). Override entries here only when
|
|
7
|
+
# you need a fixed development-specific value that cannot come from
|
|
8
|
+
# an environment variable.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Environment overlay for production.
|
|
2
|
+
# Loaded when CONFIG_ENV (or NODE_ENV) is "production".
|
|
3
|
+
# Values here override application.conf.
|
|
4
|
+
#
|
|
5
|
+
# Most production-sensitive values should be supplied via environment
|
|
6
|
+
# variables — see the ${?VAR} substitutions in application.conf.
|
|
7
|
+
#
|
|
8
|
+
# Override entries here only when a value must be fixed per-environment
|
|
9
|
+
# rather than per-deployment.
|
|
@@ -8,6 +8,13 @@ services:
|
|
|
8
8
|
volumes:
|
|
9
9
|
- ./src:/home/node/src
|
|
10
10
|
- ./config:/home/node/config
|
|
11
|
+
environment:
|
|
12
|
+
# application.conf binds loopback by default, which inside a container
|
|
13
|
+
# means "reachable from nothing" — the published port would never connect.
|
|
14
|
+
# Binding all interfaces is therefore an explicit opt-in per deployment,
|
|
15
|
+
# and it is why HTTP_CALLER_AUTH_TOKEN is worth setting in .env: past the
|
|
16
|
+
# container boundary, anyone who can reach the port can ask for decisions.
|
|
17
|
+
- HTTP_HOSTNAME=0.0.0.0
|
|
11
18
|
env_file:
|
|
12
19
|
- path: .env
|
|
13
20
|
required: false
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@o3co/auth-policy-verifier-standalone",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"packageManager": "pnpm@10.29.3",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "tsc",
|
|
13
|
+
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
|
|
13
14
|
"start": "node dist/main.mjs",
|
|
14
15
|
"test": "vitest run",
|
|
15
16
|
"debug": "NODE_OPTIONS='--conditions=development' tsx watch src/main.mts"
|
|
@@ -18,17 +19,18 @@
|
|
|
18
19
|
"@o3co/auth.policy-verifier.core": "workspace:*",
|
|
19
20
|
"@o3co/auth.policy-verifier.builtins": "workspace:*",
|
|
20
21
|
"@o3co/auth.policy-verifier.server": "workspace:*",
|
|
21
|
-
"@o3co/auth.utils": "^0.0.
|
|
22
|
-
"@o3co/ts.hocon": "^1.
|
|
23
|
-
"express": "^5.2.1"
|
|
22
|
+
"@o3co/auth.utils": "^0.0.4",
|
|
23
|
+
"@o3co/ts.hocon": "^1.12.0",
|
|
24
|
+
"express": "^5.2.1",
|
|
25
|
+
"pino": "^10.3.1"
|
|
24
26
|
},
|
|
25
27
|
"devDependencies": {
|
|
26
|
-
"@types/node": "^
|
|
27
|
-
"@types/supertest": "^7.2.
|
|
28
|
-
"jose": "^6.2.
|
|
28
|
+
"@types/node": "^26.2.0",
|
|
29
|
+
"@types/supertest": "^7.2.1",
|
|
30
|
+
"jose": "^6.2.8",
|
|
29
31
|
"supertest": "^7.2.2",
|
|
30
|
-
"tsx": "^4.
|
|
32
|
+
"tsx": "^4.23.11",
|
|
31
33
|
"typescript": "^5.9.3",
|
|
32
|
-
"vitest": "^4.1.
|
|
34
|
+
"vitest": "^4.1.10"
|
|
33
35
|
}
|
|
34
36
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 1o1 Co. Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { resolveConfigPaths } from "../configPath.js";
|
|
6
|
+
|
|
7
|
+
describe("resolveConfigPaths", () => {
|
|
8
|
+
it("accepts a configDirPath with a trailing slash", () => {
|
|
9
|
+
const { applicationConfPath, envConfPath } = resolveConfigPaths(
|
|
10
|
+
"/home/node/templates/standalone/config/",
|
|
11
|
+
"production",
|
|
12
|
+
);
|
|
13
|
+
expect(applicationConfPath).toBe("/home/node/templates/standalone/config/application.conf");
|
|
14
|
+
expect(envConfPath).toBe("/home/node/templates/standalone/config/production.conf");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("accepts a configDirPath without a trailing slash", () => {
|
|
18
|
+
const { applicationConfPath, envConfPath } = resolveConfigPaths(
|
|
19
|
+
"/home/node/templates/standalone/config",
|
|
20
|
+
"development",
|
|
21
|
+
);
|
|
22
|
+
expect(applicationConfPath).toBe("/home/node/templates/standalone/config/application.conf");
|
|
23
|
+
expect(envConfPath).toBe("/home/node/templates/standalone/config/development.conf");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("rejects env names that resolve outside configDirPath", () => {
|
|
27
|
+
expect(() =>
|
|
28
|
+
resolveConfigPaths("/home/node/templates/standalone/config/", "../secrets"),
|
|
29
|
+
).toThrow(/resolves outside/);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("rejects env names containing a path separator", () => {
|
|
33
|
+
expect(() =>
|
|
34
|
+
resolveConfigPaths("/home/node/templates/standalone/config/", "nested/env"),
|
|
35
|
+
).toThrow(/resolves outside/);
|
|
36
|
+
});
|
|
37
|
+
});
|