@pikku/azure-functions 0.12.3 → 0.12.5

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
@@ -1,3 +1,327 @@
1
+ ## 0.12.5
2
+
3
+ ### Patch Changes
4
+
5
+ - 78b29f0: `SecretService` now returns a `SecretValue<T>` rather than the bare value, so a
6
+ vault secret cannot reach a sink by accident.
7
+
8
+ `SecretValue` is nominally typed, which means it is not assignable to `string`
9
+ (or to any other concretely-typed field). Every sink with a real type — a
10
+ database column, an email body, a session payload — rejects it with no lint
11
+ rule involved. The sinks typed `any`, `unknown`, or a free generic — the logger,
12
+ queue payloads, webhook and email inputs, and a function's own output — are
13
+ guarded with `Safe<T>`, which collapses a `SecretValue` found anywhere inside
14
+ `T`, however deeply nested, to `never`.
15
+
16
+ Unwrap deliberately at the point the secret reaches the wire:
17
+
18
+ ```ts
19
+ const secret = await secrets.getSecret('BETTER_AUTH_SECRET')
20
+ betterAuth({ secret: secret.reveal() })
21
+ ```
22
+
23
+ Two behaviours cover what types cannot see. Structured serialization redacts —
24
+ `JSON.stringify` and node's inspect both yield `[secret]`, so an audit or log
25
+ write stays honest without crashing the request. String coercion throws
26
+ `SecretCoercionError`, because a template literal is always a leak.
27
+
28
+ `AuditLog.write` is guarded the same way as the logger, since an audit event
29
+ carries `input` and `metadata` as `unknown` and nominality alone cannot stop a
30
+ secret landing in one.
31
+
32
+ `.reveal()` is the deliberate escape hatch, and what it hands back is an
33
+ ordinary string as far as every sink signature is concerned. **PKU953** closes
34
+ that gap: under `pikku all --security` the inspector reports a revealed secret
35
+ that flows into a logger, an audit, a queue, an email or a webhook — `console` included.
36
+
37
+ This also fixed a real one: `remote-addon-auth.ts` called `String(token)` on an
38
+ `unknown` and wrote the result straight into an `Authorization` header.
39
+
40
+ - Updated dependencies [62ea4cc]
41
+ - Updated dependencies [9dddff8]
42
+ - Updated dependencies [78b29f0]
43
+ - @pikku/core@0.12.76
44
+
45
+ ## 0.12.4
46
+
47
+ ### Patch Changes
48
+
49
+ - afef587: Close eleven security weaknesses found in a review of `@pikku/core`. Most are
50
+ breaking, and two invalidate data or credentials already in the wild — read the
51
+ migration notes before upgrading.
52
+
53
+ **Breaking: AI agent thread ownership now fails closed.** Reading, listing,
54
+ resuming or approving an existing thread or run requires a resolved session
55
+ principal (`userId`, or `orgId` for `sessionScope: 'org'`), regardless of the
56
+ agent's `auth` setting. Previously a request without a session had no ownership
57
+ model at all: the caller-supplied `resourceId` was accepted as the ownership
58
+ key, so any caller could read or resume another party's thread by naming its
59
+ `resourceId`. Worse, `threadOwnerConstraint` returned `undefined` for a
60
+ sessionless caller, and `undefined` means _no filter_ rather than _no rows_ —
61
+ so `getAgentThreads` returned every thread in the deployment. It now returns
62
+ `string[]`, empty for a sessionless caller, which every storage backend already
63
+ treats as matching nothing. Sessionless agents still run one-shot conversations,
64
+ each with a fresh unguessable owner; what they lose is cross-request continuity.
65
+ Wire a session to restore it.
66
+
67
+ **Breaking: stored secrets and credentials must be re-entered.** `deriveKey` ran
68
+ a single unsalted round of SHA-256 over the passphrase and used the digest
69
+ directly as the AES-GCM key — roughly one hash per brute-force guess, with one
70
+ rainbow table working against every deployment. It is now PBKDF2-HMAC-SHA256 at
71
+ 600,000 iterations over a random salt. There is no compatibility path: every
72
+ value held by the kysely, mongodb and redis secret services and the kysely
73
+ credential service becomes undecryptable. They fail loud, naming the key and
74
+ `key_version`, so the app hard-fails on first secret read until each is re-set.
75
+
76
+ The KEK salt is scoped to the key version and stored alongside it, rather than
77
+ per secret, so a bulk read costs one derivation instead of N — `getSecrets` over
78
+ 50 secrets went from ~2.3s to ~48ms, and rotation from ~4.6s to ~94ms. This adds
79
+ a salt table (kysely), hash field (redis) or collection (mongodb), created
80
+ automatically on first use.
81
+
82
+ **Breaking: `PIKKU_REMOTE_SECRET` must be at least 32 characters.** The
83
+ remote-RPC session envelope moved from PBKDF2 to HKDF, which expands
84
+ high-entropy key material rather than stretching a low-entropy passphrase. That
85
+ took a remote hop from ~269ms to ~0.4ms — PBKDF2 was running twice per request —
86
+ but HKDF supplies no brute-force resistance, so the secret must carry the
87
+ entropy itself. A shorter secret now throws `WeakKeyMaterialError` at both ends.
88
+ Generate one with `openssl rand -base64 32` and roll it out to every service in
89
+ the mesh together: existing bearer tokens are format-incompatible, so a partial
90
+ rollout produces 401s until every instance is updated. The Cloudflare, Lambda
91
+ and Azure deployment services each hand-rolled a copy of `buildRemoteHeaders`
92
+ and now call the shared one, which is what keeps the two sides in step.
93
+
94
+ **Breaking: previously signed content URLs stop verifying.** `LocalContent`
95
+ signed only `{signedAt, expiresAt, notBefore}`, so a signature proved when a URL
96
+ was issued but never what it was issued for — any valid token was a skeleton
97
+ key, and swapping the pathname from a public thumbnail to a private document
98
+ still verified. The signature now binds the request path. Separately, the
99
+ verifier returned "valid" when no JWT service was wired, which is how
100
+ `pikku serve` ran: a forged `?signedAt=0&expiresAt=99999999999999` was accepted.
101
+ It now rejects with 403, `LocalContent` requires a `JWTService`, and
102
+ `pikku serve`/`pikku dev` mint an ephemeral per-process signing key so local
103
+ development works without shipping a fail-open path. In-flight signed URLs must
104
+ be re-issued.
105
+
106
+ **Request body size limits now apply to every adapter.** The `maxBodySize` cap
107
+ existed only in `PikkuFetchHTTPRequest`. The real hole was uWebSockets, which
108
+ drove `res.onData` itself and concatenated every chunk with no bound and nothing
109
+ downstream able to intervene; it now drops chunks past the limit and replies 413
110
+ before routing. Fastify delegates to its native `bodyLimit` (set only when
111
+ `maxBodySize` is configured, so fastify's stricter 1 MB default is never
112
+ loosened), and `PikkuExpressServer` feeds the limit into its body parsers. Two
113
+ paths can only reject rather than prevent, and are documented as such:
114
+ `express-middleware` mounted on your own app receives an already-parsed body, so
115
+ that deployment must bound its own parser; Next server actions bottom out at
116
+ `experimental.serverActions.bodySizeLimit`.
117
+
118
+ **Breaking: the console addon's privileged functions are gated by default.**
119
+ `wireAddon` gains a `scopes` option that applies to every function in the
120
+ addon's namespace, and the console scaffold now generates
121
+ `wireAddon({ name: 'console', package: '@pikku/addon-console', scopes: ['admin'] })`.
122
+ Previously the console's entire privileged surface — around 54 functions
123
+ including `credentialGet`, which returns a resolved OAuth token for an arbitrary
124
+ `userId`, `updateFunctionBody`, and `installAddon`, which shells out to a
125
+ package install — was protected only by an optional host-registered
126
+ `addGlobalPermission`. `resolveGlobalPermissions` returns `[]` when none is
127
+ registered and permission checking then no-ops, so an app that never registered
128
+ one served those functions to anyone, and with the template's default
129
+ `scaffold.rpc: "no-auth"` that meant unauthenticated. All of them now return 403
130
+ `MissingScopeError` without an `admin`, `admin:*` or `*` scope. **Regenerating
131
+ is required** — an app holding an old `console.gen.ts` stays open.
132
+ `installAddon` and `installOpenapiAddon` additionally declare their own
133
+ `auth: true, scopes: ['admin']`, and `getAgentThreads` now scopes its listing to
134
+ the session's own threads unless the caller holds admin.
135
+
136
+ Addon scopes are enforced in `runPikkuFunc` rather than at the RPC boundary,
137
+ because a wiring can reference an addon function directly — the inspector
138
+ records the addon's `packageName` on HTTP, channel, schedule, queue, CLI,
139
+ trigger, gateway and MCP wirings — and those paths never call `resolveNamespace`.
140
+ Enforcing at the RPC seam would have covered only the `namespace:function` form
141
+ while reading as complete.
142
+
143
+ **Breaking: `wireAddon`'s `auth` and `tags` now apply on direct wirings too.**
144
+ Both were read only by `resolveAddonFunction`, so they had exactly the hole
145
+ scopes had: `wireAddon({ name: 'console', package: '@pikku/addon-console', auth:
146
+ true, tags: ['admin'] })` gated `rpc('console:credentialGet')` and gated nothing
147
+ at all on an HTTP route wired straight to `credentialGet`. A consumer who
148
+ reached for the documented way to lock an addon down got a control that was
149
+ silently inert on every wiring except one. Both now resolve in `runPikkuFunc`.
150
+ `auth` merges as an OR — `auth: false` from an addon is ignored on a direct
151
+ wiring, because an addon may require a session the wiring did not but must never
152
+ waive one it did. Addon tags resolve to concrete middleware against the **root**
153
+ tag groups before the call rather than being folded into the function's
154
+ inherited middleware: `addTagMiddleware('admin', …)` is written by the consuming
155
+ app and registers under the root package, whereas `combineMiddleware` would look
156
+ the tag up under the addon's own `packageName`, where it does not exist.
157
+
158
+ One consequence worth naming: an addon that wires `auth: true` and also runs its
159
+ own sessionless internal work — a scheduled task or queue worker inside the
160
+ addon calling a sibling function — is now gated where it previously was not,
161
+ because a bare `rpc('fn')` from inside the addon reaches `runPikkuFunc` with the
162
+ addon's `packageName` like any other call. This reverses a decision that
163
+ deliberately scoped the gate to the namespaced boundary; that reasoning held
164
+ only while the boundary was real, and a direct wiring can enter an addon without
165
+ crossing it, so "already inside" is not something the runtime can infer. Such an
166
+ addon should carry authorization on the function via
167
+ `pikkuFunc({ permissions })`, which has always been enforced on every path. A
168
+ follow-up will add execution provenance so an intra-addon call can be
169
+ distinguished from an external one and skip the addon-level check; that needs a
170
+ marker no caller outside the process can set, which is its own design problem
171
+ and does not belong in a security fix.
172
+
173
+ **Codegen now warns when an exposed function has no gate (PKU574).** The
174
+ generated `POST /rpc/:rpcName` dispatcher forwards to `rpc.exposed`, which
175
+ refuses anything without `expose: true` — but nothing checked whether the
176
+ target was gated, because a dispatcher cannot know what it dispatches to. The
177
+ console shipped ~54 privileged functions through that gap and the toolchain was
178
+ silent. The inspector now reports every function that is exposed, sessionless,
179
+ and carries no `auth`, `scopes` or `permissions` of its own and none from a
180
+ governing `wireAddon`. It is a `warn`, not a critical: `expose: true` on an
181
+ ungated sessionless function is correct for a genuinely public endpoint, so it
182
+ blocks a build only under `--fail-on-warn`.
183
+
184
+ Two pieces of metadata were missing for this to be answerable statically, and
185
+ both are now recorded. A `pikkuSessionlessFunc`'s own `auth: true` was read at
186
+ runtime but never written to function meta, so a self-gated function was
187
+ indistinguishable from an ungated one — `sessionless` carries the baseline, and
188
+ `auth` now carries the tightening. And `wireAddon`'s `scopes`, `auth` and `tags`
189
+ were not parsed at all: the inspector recorded the addon's `rpcEndpoint` and its
190
+ secret, variable and credential overrides, and dropped every one of its gates.
191
+ An addon whose gates are not statically knowable is treated as gated, because a
192
+ false positive on a correctly-secured addon costs more than the one case it
193
+ would catch.
194
+
195
+ **Breaking: an application's global permissions now apply inside addons.**
196
+ `resolveGlobalPermissions` read only the bucket matching the function's own
197
+ package, but the generated `addGlobalPermission` wrapper takes no package
198
+ argument and always registers under the root. An app-wide rule like "every
199
+ request needs a signed-in user" therefore stopped at the addon boundary, and the
200
+ bucket an addon's functions did read was one no host could write to — which is
201
+ why the console addon's recommended `addGlobalPermission([isAdmin],
202
+ '@pikku/addon-console')` was never a gate anybody could actually install. A
203
+ function now resolves the root bucket and its own package's, root first.
204
+ Unioning is safe in a way nothing else here would be: globals AND, so adding the
205
+ root ones can only tighten. Package buckets stay one-way — a package's globals
206
+ never reach root functions, or an installed addon could gate the whole
207
+ application. Apps with both a root global and addon-provided functions will see
208
+ those functions gated where they previously were not.
209
+
210
+ **Codegen now records whether each HTTP route requires a session, and warns
211
+ about inert addon tags (PKU575).** Four separate things can demand a session —
212
+ the function's `sessionless`, its own `auth`, the route's (or its group's)
213
+ `auth`, and the addon it belongs to — and answering "which routes are open?"
214
+ meant joining all four by hand and knowing which wins. Each route's meta now
215
+ carries the resolved `requiresSession` alongside the route's own `auth`. Scopes
216
+ count as requiring one, since they are matched against the session's and fail
217
+ closed. Anything not statically knowable resolves to `true`, matching PKU574:
218
+ a route that looks stricter than it is costs less than one that looks open and
219
+ isn't. Separately, `wireAddon({ tags: ['admin'] })` reads like a gate and is
220
+ applied like one right up until no `addTagMiddleware('admin', …)` exists, at
221
+ which point it resolves to an empty list and gates nothing; that now warns.
222
+ Only addon tags are reported — a tag on a function is as likely to be
223
+ organizational, and warning about those would bury the case that matters.
224
+
225
+ **Object-shorthand permissions were missing from meta.** The inspector visited
226
+ `ts.PropertyAssignment` but not `ts.ShorthandPropertyAssignment`, so
227
+ `permissions: { canAdminOrg }` — enforced identically to the longhand form at
228
+ runtime, since `verifyPermissions` has a non-array branch — was recorded as _no
229
+ permissions at all_. That is the most dangerous direction for meta to be wrong
230
+ in: an audit reading it sees an open door where one is shut. It cost this review
231
+ a false IDOR report across ~35 billing and org functions before the source
232
+ contradicted the metadata.
233
+
234
+ **Functions that authorize in their own body can say so.** A webhook receiver
235
+ verifying a signature, or a handler redeeming a signed invite, is genuinely
236
+ closed while carrying no session, scope or permission — indistinguishable in
237
+ meta from one nobody remembered to gate, and so warned about forever by PKU574.
238
+ `selfAuthenticated: true` on the function config records the claim and silences
239
+ the warning for that function. It is declarative only: nothing at runtime reads
240
+ it and it grants nothing. Detection was rejected deliberately — inferring it
241
+ from the body means a function that _looks_ like it checks something silences
242
+ the warning while checking nothing, and a warning that is usually wrong stops
243
+ being read.
244
+
245
+ **Breaking: `scaffold.<feature>` is now `boolean | { auth?, path? }`, and `true`
246
+ means authenticated.** The old `'auth' | 'no-auth' | false` read like a
247
+ starter-file preference while being a live authorization decision, set three
248
+ directories from the functions it governed — the shape the console incident
249
+ took. A surface now becomes public only by writing `{ auth: false }`, so
250
+ omitting a field can never open anything: the failure mode of a forgotten flag
251
+ is a locked door. `{ path }` additionally overrides where the file is
252
+ generated, which previously could only be set for all features at once via
253
+ `pikkuDir`.
254
+
255
+ The legacy strings are **refused, not coerced**. `resolveScaffoldFeature` throws
256
+ naming the key and its replacement (`"rpc": "no-auth"` → `"rpc": { "auth":
257
+ false }`), and it does so at config load, not downstream. An earlier design used
258
+ a bare `string` for the output path, under which `"no-auth"` would have parsed
259
+ as a file named `no-auth` and every unmigrated config would have silently
260
+ produced nonsense; the object form makes any string invalid, so the failure is
261
+ loud.
262
+
263
+ The collapse is deliberately not uniform in effect. For `rpc` the flag was a
264
+ blanket "no anonymous RPC in this app" set on a dispatcher that cannot know what
265
+ it dispatches to; for `userAdmin` it was redundant, since the generated
266
+ functions are already `pikkuFunc` with `scopes: ['admin:users:list']`. But for
267
+ `agent`, `workflow`, `events` and `scenarios` it is the only gate — those
268
+ generate real endpoints the app never authors — so `true` keeps them
269
+ authenticated rather than opening them as a side effect of a config cleanup.
270
+ `webhook` and `remoteRpc` have no auth dimension at all (`serialize-remote-rpc.ts`
271
+ hardcodes `auth: false`), so an `{ auth }` on them is ignored. The three
272
+ configs in this repo are migrated preserving their current behaviour exactly.
273
+
274
+ **Queue job identities are signed.** A job carried the producer's `pikkuUserId`
275
+ as a plain string and the worker resolved a session from it with no
276
+ verification, so write access to the queue backend was act-as-any-user. The
277
+ identity is now `pq1.<claim>.<hmac>`, HMAC-SHA256 over the claim and the
278
+ canonicalized job payload, keyed by HKDF expansion of a new
279
+ `PIKKU_QUEUE_IDENTITY_SECRET`. Producers opt in by wrapping their queue service
280
+ with `SignedQueueService`. This fails safe rather than closed: with no secret
281
+ configured the identity is dropped and jobs still process, warning once per
282
+ process, so no existing deployment breaks on upgrade — it simply loses queue
283
+ identity until the secret is set. The payload rather than the job id is bound
284
+ because SQS, Cloudflare Queues, Azure and the in-memory service all mint ids
285
+ after `add` returns.
286
+
287
+ **Workflow inline state is read from the run record.** `isInline` consulted a
288
+ process-local `Map`, while `WorkflowRun.inline` is durable. Any instance that
289
+ did not start a run disagreed with the record, so one instance could dispatch a
290
+ queued job for a workflow another was already executing in-process. It is now
291
+ async and resolves through the durable identity, cached only when a context
292
+ already exists so a passive reader allocates nothing. The same `Map` also leaked:
293
+ `nextStepKey` fabricated replay state on every step, and `releaseContext`
294
+ refused to free anything carrying it, so runs whose steps executed outside a
295
+ `beginReplay` bracket — the step-worker queue path — stranded their context and
296
+ step state for the process lifetime. Contexts are now released by an explicit
297
+ execution counter. Step ordinals reset per execution rather than accumulating
298
+ across step-worker invocations in one process, which makes step naming
299
+ independent of how work was distributed.
300
+
301
+ **Secret reads fail loud in every store.** `MongoDBSecretService.getSecrets`
302
+ skipped rows that failed to decrypt, and the redis equivalent dropped every
303
+ rejection via `Promise.allSettled`, including the "No KEK available for
304
+ key_version N" configuration error. Both now throw, naming the key and its key
305
+ version, matching the kysely behaviour. This matters most alongside the KEK
306
+ change above: without it, an upgrade surfaces as a partial secrets map and an
307
+ opaque downstream failure instead of an error naming the secret to re-enter.
308
+
309
+ **A second middleware registration for a pattern no longer erases the first.**
310
+ `addHTTPMiddleware`, `addTagMiddleware` and `addChannelMiddleware` groups are
311
+ keyed by pattern or tag and held one source file each, so a second file's call
312
+ overwrote the first's. Codegen emits its imports from what is stored, so the
313
+ losing file was never imported and its middleware never registered — the
314
+ runtime composes repeated registrations for a pattern happily, and only codegen
315
+ dropped one. Adding an unrelated `addHTTPMiddleware('*', …)` to an app was
316
+ enough to silently unregister the generated better-auth session bridge, which
317
+ fails open and gives no sign until a request arrives without a session. A group
318
+ now carries every registration made for it, and all of them are imported.
319
+
320
+ - Updated dependencies [6a307f0]
321
+ - Updated dependencies [afef587]
322
+ - Updated dependencies [8075f6a]
323
+ - @pikku/core@0.12.74
324
+
1
325
  ## 0.12.3
2
326
 
3
327
  ### Patch Changes
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 - present Yasser Fadl and Pikku contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -0,0 +1,35 @@
1
+ # @pikku/azure-functions
2
+
3
+ Azure Functions runtime for Pikku — HTTP, timer and storage-queue handlers,
4
+ plus `AzureQueueService`.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npm install @pikku/azure-functions
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```typescript
15
+ import { app } from '@azure/functions'
16
+ import { createAzureHandler } from '@pikku/azure-functions'
17
+
18
+ import './.pikku/pikku-bootstrap.gen.js'
19
+
20
+ const handlers = createAzureHandler(factories, ['fetch'])
21
+
22
+ app.http('pikku', {
23
+ route: '{*path}',
24
+ authLevel: 'anonymous',
25
+ handler: handlers.http,
26
+ })
27
+ ```
28
+
29
+ `createAzureWorkerHandler` and `createAzureWebSocketHandler` cover queue and
30
+ WebSocket units. Use `@pikku/deploy-azure` to generate `host.json` and the
31
+ infra manifest.
32
+
33
+ ## Docs
34
+
35
+ https://pikku.dev/docs
@@ -1,4 +1,4 @@
1
- import { encryptJSON } from '@pikku/core/crypto-utils';
1
+ import { buildRemoteHeaders } from '@pikku/core/remote';
2
2
  export class AzureDeploymentService {
3
3
  jwt;
4
4
  secrets;
@@ -17,27 +17,7 @@ export class AzureDeploymentService {
17
17
  throw new Error(`No Azure Function binding for function '${funcName}'. ` +
18
18
  `Available bindings: ${[...this.bindings.keys()].join(', ')}`);
19
19
  }
20
- const headers = {
21
- 'content-type': 'application/json',
22
- ...(traceId && { 'x-request-id': traceId }),
23
- };
24
- let secret;
25
- try {
26
- secret = await this.secrets.getSecret('PIKKU_REMOTE_SECRET');
27
- }
28
- catch { }
29
- if (secret && this.jwt) {
30
- const sessionEnc = session
31
- ? await encryptJSON(secret, { session })
32
- : undefined;
33
- const token = await this.jwt.encode({ value: 5, unit: 'minute' }, {
34
- aud: 'pikku-remote',
35
- fn: funcName,
36
- iat: Math.floor(Date.now() / 1000),
37
- session: sessionEnc,
38
- });
39
- headers.authorization = `Bearer ${token}`;
40
- }
20
+ const headers = await buildRemoteHeaders(this.jwt, this.secrets, funcName, session, traceId);
41
21
  const url = `${functionUrl}/remote/rpc/${encodeURIComponent(funcName)}`;
42
22
  const response = await fetch(url, {
43
23
  method: 'POST',
@@ -1 +1 @@
1
- {"version":3,"file":"azure-deployment-service.js","sourceRoot":"","sources":["../src/azure-deployment-service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"azure-deployment-service.js","sourceRoot":"","sources":["../src/azure-deployment-service.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAEvD,MAAM,OAAO,sBAAsB;IAIvB;IACA;IAJF,QAAQ,CAAqB;IAErC,YACU,GAA2B,EAC3B,OAAsB,EAC9B,gBAAwC;QAFhC,QAAG,GAAH,GAAG,CAAwB;QAC3B,YAAO,GAAP,OAAO,CAAe;QAG9B,IAAI,CAAC,QAAQ,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAA;IAC3D,CAAC;IAED,KAAK,CAAC,IAAI,KAAmB,CAAC;IAC9B,KAAK,CAAC,KAAK,CAAC,OAAyB,IAAkB,CAAC;IACxD,KAAK,CAAC,IAAI,KAAmB,CAAC;IAE9B,KAAK,CAAC,MAAM,CACV,QAAgB,EAChB,IAAa,EACb,OAAiB,EACjB,OAAgB;QAEhB,MAAM,WAAW,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QAC/C,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CACb,2CAA2C,QAAQ,KAAK;gBACtD,uBAAuB,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAChE,CAAA;QACH,CAAC;QAED,MAAM,OAAO,GAAG,MAAM,kBAAkB,CACtC,IAAI,CAAC,GAAG,EACR,IAAI,CAAC,OAAO,EACZ,QAAQ,EACR,OAAO,EACP,OAAO,CACR,CAAA;QAED,MAAM,GAAG,GAAG,GAAG,WAAW,eAAe,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAAA;QACvE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAChC,MAAM,EAAE,MAAM;YACd,OAAO;YACP,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,CAAC;SAC/B,CAAC,CAAA;QAEF,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,IAAI,SAAiB,CAAA;YACrB,IAAI,CAAC;gBACH,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;YACnD,CAAC;YAAC,MAAM,CAAC;gBACP,SAAS,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;YACnC,CAAC;YACD,MAAM,IAAI,KAAK,CACb,uBAAuB,QAAQ,aAAa,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,KAAK,SAAS,EAAE,CACnG,CAAA;QACH,CAAC;QAED,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAA;IACxB,CAAC;CACF"}
@@ -1,12 +1,23 @@
1
1
  import type { InvocationContext } from '@azure/functions';
2
2
  import type { Logger, LogLevel } from '@pikku/core/services';
3
+ import type { Safe } from '@pikku/core';
3
4
  export declare class AzInvocationLogger implements Logger {
4
5
  private context;
5
6
  constructor(context: InvocationContext);
6
- info(messageOrObj: string | Record<string, any>): void;
7
- warn(messageOrObj: string | Record<string, any>): void;
8
- error(messageOrObj: string | Record<string, any> | Error): void;
9
- debug(message: string): void;
10
- trace(message: string): void;
7
+ info<M extends string | Record<string, any>, A extends unknown[]>(messageOrObj: Safe<M>, ...meta: {
8
+ [K in keyof A]: Safe<A[K]>;
9
+ }): void;
10
+ warn<M extends string | Record<string, any>, A extends unknown[]>(messageOrObj: Safe<M>, ...meta: {
11
+ [K in keyof A]: Safe<A[K]>;
12
+ }): void;
13
+ error<M extends string | Record<string, any> | Error, A extends unknown[]>(messageOrObj: Safe<M>, ...meta: {
14
+ [K in keyof A]: Safe<A[K]>;
15
+ }): void;
16
+ debug<A extends unknown[]>(message: string, ...meta: {
17
+ [K in keyof A]: Safe<A[K]>;
18
+ }): void;
19
+ trace<A extends unknown[]>(message: string, ...meta: {
20
+ [K in keyof A]: Safe<A[K]>;
21
+ }): void;
11
22
  setLevel(level: LogLevel): void;
12
23
  }
@@ -3,20 +3,20 @@ export class AzInvocationLogger {
3
3
  constructor(context) {
4
4
  this.context = context;
5
5
  }
6
- info(messageOrObj) {
7
- this.context.info(messageOrObj);
6
+ info(messageOrObj, ...meta) {
7
+ this.context.info(messageOrObj, ...meta);
8
8
  }
9
- warn(messageOrObj) {
10
- this.context.warn(messageOrObj);
9
+ warn(messageOrObj, ...meta) {
10
+ this.context.warn(messageOrObj, ...meta);
11
11
  }
12
- error(messageOrObj) {
13
- this.context.error(messageOrObj);
12
+ error(messageOrObj, ...meta) {
13
+ this.context.error(messageOrObj, ...meta);
14
14
  }
15
- debug(message) {
16
- this.context.debug(message);
15
+ debug(message, ...meta) {
16
+ this.context.debug(message, ...meta);
17
17
  }
18
- trace(message) {
19
- this.context.trace(message);
18
+ trace(message, ...meta) {
19
+ this.context.trace(message, ...meta);
20
20
  }
21
21
  setLevel(level) {
22
22
  }
@@ -1 +1 @@
1
- {"version":3,"file":"pikku-az-functions-logger.js","sourceRoot":"","sources":["../src/pikku-az-functions-logger.ts"],"names":[],"mappings":"AAGA,MAAM,OAAO,kBAAkB;IAGT;IAApB,YAAoB,OAA0B;QAA1B,YAAO,GAAP,OAAO,CAAmB;IAAG,CAAC;IAE3C,IAAI,CAAC,YAA0C;QACpD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;IACjC,CAAC;IAEM,IAAI,CAAC,YAA0C;QACpD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;IACjC,CAAC;IAEM,KAAK,CAAC,YAAkD;QAC7D,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAA;IAClC,CAAC;IAEM,KAAK,CAAC,OAAe;QAC1B,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IAC7B,CAAC;IAEM,KAAK,CAAC,OAAe;QAC1B,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IAC7B,CAAC;IAEM,QAAQ,CAAC,KAAe;IAE/B,CAAC;CACF"}
1
+ {"version":3,"file":"pikku-az-functions-logger.js","sourceRoot":"","sources":["../src/pikku-az-functions-logger.ts"],"names":[],"mappings":"AAIA,MAAM,OAAO,kBAAkB;IAGT;IAApB,YAAoB,OAA0B;QAA1B,YAAO,GAAP,OAAO,CAAmB;IAAG,CAAC;IAE3C,IAAI,CACT,YAAqB,EACrB,GAAG,IAAoC;QAEvC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,YAAmB,EAAE,GAAG,IAAI,CAAC,CAAA;IACjD,CAAC;IAEM,IAAI,CACT,YAAqB,EACrB,GAAG,IAAoC;QAEvC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,YAAmB,EAAE,GAAG,IAAI,CAAC,CAAA;IACjD,CAAC;IAEM,KAAK,CAGV,YAAqB,EAAE,GAAG,IAAoC;QAC9D,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,YAAmB,EAAE,GAAG,IAAI,CAAC,CAAA;IAClD,CAAC;IAEM,KAAK,CACV,OAAe,EACf,GAAG,IAAoC;QAEvC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,CAAA;IACtC,CAAC;IAEM,KAAK,CACV,OAAe,EACf,GAAG,IAAoC;QAEvC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,CAAA;IACtC,CAAC;IAEM,QAAQ,CAAC,KAAe;IAE/B,CAAC;CACF"}
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@pikku/azure-functions",
3
- "version": "0.12.3",
3
+ "version": "0.12.5",
4
+ "description": "Azure Functions runtime for Pikku — HTTP, timer and storage-queue handlers",
4
5
  "license": "MIT",
5
6
  "author": "yasser.fadl@gmail.com",
6
7
  "module": "dist/index.js",
7
8
  "main": "dist/index.js",
8
9
  "type": "module",
9
10
  "scripts": {
10
- "tsc": "tsc",
11
+ "tsc": "tsc && tsc -p tsconfig.type-tests.json",
11
12
  "ncu": "npx npm-check-updates",
12
13
  "build": "tsc -b",
13
14
  "release": "npm run build && npm test",
@@ -17,7 +18,7 @@
17
18
  "prepublishOnly": "yarn build"
18
19
  },
19
20
  "peerDependencies": {
20
- "@pikku/core": "^0.12.58"
21
+ "@pikku/core": "^0.12.76"
21
22
  },
22
23
  "devDependencies": {
23
24
  "typescript": "^6.0.3"
@@ -12,7 +12,7 @@
12
12
  import type { DeploymentService, DeploymentConfig } from '@pikku/core/services'
13
13
  import type { JWTService } from '@pikku/core/services'
14
14
  import type { SecretService } from '@pikku/core/services'
15
- import { encryptJSON } from '@pikku/core/crypto-utils'
15
+ import { buildRemoteHeaders } from '@pikku/core/remote'
16
16
 
17
17
  export class AzureDeploymentService implements DeploymentService {
18
18
  private bindings: Map<string, string>
@@ -43,32 +43,13 @@ export class AzureDeploymentService implements DeploymentService {
43
43
  )
44
44
  }
45
45
 
46
- const headers: Record<string, string> = {
47
- 'content-type': 'application/json',
48
- ...(traceId && { 'x-request-id': traceId }),
49
- }
50
-
51
- // Sign session as JWT for pikkuRemoteAuthMiddleware
52
- let secret: string | undefined
53
- try {
54
- secret = await this.secrets.getSecret('PIKKU_REMOTE_SECRET')
55
- } catch {}
56
-
57
- if (secret && this.jwt) {
58
- const sessionEnc = session
59
- ? await encryptJSON(secret, { session })
60
- : undefined
61
- const token = await this.jwt.encode(
62
- { value: 5, unit: 'minute' },
63
- {
64
- aud: 'pikku-remote',
65
- fn: funcName,
66
- iat: Math.floor(Date.now() / 1000),
67
- session: sessionEnc,
68
- }
69
- )
70
- headers.authorization = `Bearer ${token}`
71
- }
46
+ const headers = await buildRemoteHeaders(
47
+ this.jwt,
48
+ this.secrets,
49
+ funcName,
50
+ session,
51
+ traceId
52
+ )
72
53
 
73
54
  const url = `${functionUrl}/remote/rpc/${encodeURIComponent(funcName)}`
74
55
  const response = await fetch(url, {
@@ -1,29 +1,45 @@
1
1
  import type { InvocationContext } from '@azure/functions'
2
2
  import type { Logger, LogLevel } from '@pikku/core/services'
3
+ import type { Safe } from '@pikku/core'
3
4
 
4
5
  export class AzInvocationLogger implements Logger {
5
6
  // private logLevel: LogLevel = LogLevel.info
6
7
 
7
8
  constructor(private context: InvocationContext) {}
8
9
 
9
- public info(messageOrObj: string | Record<string, any>): void {
10
- this.context.info(messageOrObj)
10
+ public info<M extends string | Record<string, any>, A extends unknown[]>(
11
+ messageOrObj: Safe<M>,
12
+ ...meta: { [K in keyof A]: Safe<A[K]> }
13
+ ): void {
14
+ this.context.info(messageOrObj as any, ...meta)
11
15
  }
12
16
 
13
- public warn(messageOrObj: string | Record<string, any>): void {
14
- this.context.warn(messageOrObj)
17
+ public warn<M extends string | Record<string, any>, A extends unknown[]>(
18
+ messageOrObj: Safe<M>,
19
+ ...meta: { [K in keyof A]: Safe<A[K]> }
20
+ ): void {
21
+ this.context.warn(messageOrObj as any, ...meta)
15
22
  }
16
23
 
17
- public error(messageOrObj: string | Record<string, any> | Error): void {
18
- this.context.error(messageOrObj)
24
+ public error<
25
+ M extends string | Record<string, any> | Error,
26
+ A extends unknown[],
27
+ >(messageOrObj: Safe<M>, ...meta: { [K in keyof A]: Safe<A[K]> }): void {
28
+ this.context.error(messageOrObj as any, ...meta)
19
29
  }
20
30
 
21
- public debug(message: string): void {
22
- this.context.debug(message)
31
+ public debug<A extends unknown[]>(
32
+ message: string,
33
+ ...meta: { [K in keyof A]: Safe<A[K]> }
34
+ ): void {
35
+ this.context.debug(message, ...meta)
23
36
  }
24
37
 
25
- public trace(message: string): void {
26
- this.context.trace(message)
38
+ public trace<A extends unknown[]>(
39
+ message: string,
40
+ ...meta: { [K in keyof A]: Safe<A[K]> }
41
+ ): void {
42
+ this.context.trace(message, ...meta)
27
43
  }
28
44
 
29
45
  public setLevel(level: LogLevel): void {
@@ -0,0 +1,18 @@
1
+ import { createSecretValue } from '@pikku/core'
2
+ import type { AzInvocationLogger } from './pikku-az-functions-logger.js'
3
+
4
+ // A concrete logger must not be a way around the `Logger` guard.
5
+ const _assertions = (logger: AzInvocationLogger) => {
6
+ const secret = createSecretValue('sk-live-DEADBEEF')
7
+
8
+ // @ts-expect-error a secret cannot be logged
9
+ logger.info(secret)
10
+ // @ts-expect-error nor nested in the message
11
+ logger.info({ token: secret })
12
+ // @ts-expect-error nor in the metadata
13
+ logger.warn('using', { token: secret })
14
+
15
+ logger.info('fine')
16
+ logger.info({ count: 1 })
17
+ }
18
+ void _assertions
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": true,
5
+ "composite": false,
6
+ "declaration": false,
7
+ "declarationMap": false
8
+ },
9
+ "files": [
10
+ "src/pikku-az-functions-logger.type.test.ts"
11
+ ]
12
+ }