@pikku/core 0.12.74 → 0.12.77
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 +378 -0
- package/dist/column-form.d.ts +32 -0
- package/dist/column-form.js +42 -0
- package/dist/crypto-utils.d.ts +15 -4
- package/dist/crypto-utils.js +18 -2
- package/dist/data-classification.d.ts +44 -0
- package/dist/function/functions.types.d.ts +23 -10
- package/dist/function/index.d.ts +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +3 -1
- package/dist/middleware/auth-bearer.js +2 -1
- package/dist/middleware/remote-auth.js +1 -1
- package/dist/remote.js +1 -1
- package/dist/secret-value.d.ts +56 -0
- package/dist/secret-value.js +46 -0
- package/dist/services/audit-service.d.ts +74 -4
- package/dist/services/audit-service.js +7 -5
- package/dist/services/credential-wire-service.d.ts +5 -0
- package/dist/services/credential-wire-service.js +9 -1
- package/dist/services/email-service.d.ts +2 -1
- package/dist/services/index.d.ts +3 -3
- package/dist/services/index.js +1 -1
- package/dist/services/local-content-request-handler.d.ts +29 -0
- package/dist/services/local-content-request-handler.js +176 -0
- package/dist/services/local-secrets.d.ts +4 -3
- package/dist/services/local-secrets.js +7 -3
- package/dist/services/logger.d.ts +22 -5
- package/dist/services/queue-webhook-service.js +1 -1
- package/dist/services/scoped-secret-service.d.ts +4 -3
- package/dist/services/secret-service.d.ts +8 -3
- package/dist/services/typed-secret-service.d.ts +5 -4
- package/dist/services/webhook-service.d.ts +2 -1
- package/dist/testing/service-tests.js +6 -6
- package/dist/types/core.types.d.ts +25 -4
- package/dist/wirings/ai-agent/ai-agent-agui.js +13 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +7 -1
- package/dist/wirings/ai-agent/ai-agent-runner.js +14 -2
- package/dist/wirings/ai-agent/ai-agent-stream.js +27 -1
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +40 -0
- package/dist/wirings/ai-agent/index.d.ts +1 -1
- package/dist/wirings/ai-agent/index.js +1 -1
- package/dist/wirings/ai-agent/voice-input.d.ts +20 -0
- package/dist/wirings/ai-agent/voice-input.js +44 -9
- package/dist/wirings/ai-agent/voice-output.d.ts +15 -0
- package/dist/wirings/ai-agent/voice-output.js +10 -1
- package/dist/wirings/cli/channel/cli-raw-client-runner.d.ts +21 -3
- package/dist/wirings/cli/channel/cli-raw-client-runner.js +13 -5
- package/dist/wirings/cli/channel/index.d.ts +1 -0
- package/dist/wirings/persona/define-personas.d.ts +4 -0
- package/dist/wirings/persona/define-personas.js +4 -0
- package/dist/wirings/persona/persona.types.d.ts +11 -0
- package/dist/wirings/queue/queue-identity.js +2 -1
- package/dist/wirings/queue/queue.types.d.ts +2 -1
- package/dist/wirings/queue/signed-queue-service.d.ts +2 -1
- package/dist/wirings/rpc/remote-addon-auth.d.ts +2 -1
- package/dist/wirings/rpc/remote-addon-auth.js +6 -2
- package/dist/wirings/virtual-user/index.d.ts +3 -0
- package/dist/wirings/virtual-user/index.js +2 -0
- package/dist/wirings/virtual-user/prepare-virtual-user-run.d.ts +54 -0
- package/dist/wirings/virtual-user/prepare-virtual-user-run.js +49 -0
- package/dist/wirings/virtual-user/virtual-user-run-store.d.ts +90 -0
- package/dist/wirings/virtual-user/virtual-user-run-store.js +1 -0
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +14 -10
- package/dist/wirings/workflow/pikku-scenario-service.js +1 -2
- package/dist/wirings/workflow/scenario-prose.js +1 -1
- package/dist/wirings/workflow/scenario-step.types.d.ts +13 -7
- package/dist/wirings/workflow/workflow.types.d.ts +7 -0
- package/knowledge/decisions/internals/a-virtual-user-run-is-not-a-workflow-and-not-a-queued-job.md +48 -0
- package/knowledge/decisions/internals/core-column-form-is-an-axis-of-its-own.md +84 -0
- package/knowledge/decisions/internals/core-data-classification-brand-is-an-optional-property.md +9 -2
- package/knowledge/decisions/internals/index.md +4 -0
- package/knowledge/decisions/internals/one-project-shape-check-two-validators.md +53 -0
- package/knowledge/decisions/internals/scenarios-live-in-files-named-for-them.md +48 -0
- package/knowledge/decisions/internals/validate-checks-personas-through-a-shared-module.md +43 -0
- package/package.json +3 -2
- package/src/column-form.test.ts +97 -0
- package/src/column-form.ts +58 -0
- package/src/crypto-utils.ts +25 -6
- package/src/data-classification.ts +44 -0
- package/src/function/functions.types.ts +47 -10
- package/src/function/index.ts +1 -0
- package/src/index.ts +24 -2
- package/src/middleware/auth-bearer.test.ts +3 -2
- package/src/middleware/auth-bearer.ts +2 -1
- package/src/middleware/remote-auth.test.ts +2 -1
- package/src/middleware/remote-auth.ts +1 -1
- package/src/remote.test.ts +2 -1
- package/src/remote.ts +1 -1
- package/src/secret-value.test.ts +204 -0
- package/src/secret-value.ts +111 -0
- package/src/services/audit-service.ts +87 -9
- package/src/services/credential-wire-service.ts +9 -1
- package/src/services/email-service.ts +3 -1
- package/src/services/index.ts +3 -3
- package/src/services/local-content-request-handler.test.ts +202 -0
- package/src/services/local-content-request-handler.ts +267 -0
- package/src/services/local-secrets.test.ts +20 -5
- package/src/services/local-secrets.ts +15 -7
- package/src/services/logger.ts +27 -7
- package/src/services/queue-webhook-service.test.ts +2 -1
- package/src/services/queue-webhook-service.ts +1 -1
- package/src/services/scoped-secret-service.ts +4 -3
- package/src/services/secret-service.ts +8 -3
- package/src/services/typed-secret-service.ts +11 -7
- package/src/services/webhook-service.ts +4 -1
- package/src/testing/service-tests.ts +6 -6
- package/src/types/core.types.ts +25 -4
- package/src/wirings/ai-agent/ai-agent-agui.test.ts +16 -0
- package/src/wirings/ai-agent/ai-agent-agui.ts +14 -1
- package/src/wirings/ai-agent/ai-agent-prepare.ts +7 -1
- package/src/wirings/ai-agent/ai-agent-runner.ts +18 -2
- package/src/wirings/ai-agent/ai-agent-stream.ts +32 -1
- package/src/wirings/ai-agent/ai-agent.types.ts +45 -1
- package/src/wirings/ai-agent/index.ts +2 -0
- package/src/wirings/ai-agent/voice-input.test.ts +65 -0
- package/src/wirings/ai-agent/voice-input.ts +48 -9
- package/src/wirings/ai-agent/voice-output.test.ts +91 -1
- package/src/wirings/ai-agent/voice-output.ts +28 -1
- package/src/wirings/cli/channel/cli-raw-client-runner.ts +39 -9
- package/src/wirings/cli/channel/index.ts +4 -0
- package/src/wirings/persona/define-personas.ts +4 -0
- package/src/wirings/persona/persona.types.ts +11 -0
- package/src/wirings/queue/queue-identity.test.ts +2 -1
- package/src/wirings/queue/queue-identity.ts +4 -1
- package/src/wirings/queue/queue.types.ts +6 -1
- package/src/wirings/queue/signed-queue-service.ts +2 -1
- package/src/wirings/rpc/remote-addon-auth.ts +8 -3
- package/src/wirings/rpc/rpc-runner.test.ts +6 -4
- package/src/wirings/virtual-user/index.ts +12 -0
- package/src/wirings/virtual-user/prepare-virtual-user-run.test.ts +115 -0
- package/src/wirings/virtual-user/prepare-virtual-user-run.ts +95 -0
- package/src/wirings/virtual-user/virtual-user-run-store.ts +98 -0
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +14 -16
- package/src/wirings/workflow/pikku-scenario-service.ts +1 -10
- package/src/wirings/workflow/scenario-prose.test.ts +5 -7
- package/src/wirings/workflow/scenario-prose.ts +1 -1
- package/src/wirings/workflow/scenario-service.test.ts +0 -1
- package/src/wirings/workflow/scenario-step.test.ts +4 -5
- package/src/wirings/workflow/scenario-step.types.ts +13 -7
- package/src/wirings/workflow/scenario-surface.test.ts +6 -5
- package/src/wirings/workflow/workflow.types.ts +7 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/tsconfig.type-tests.json +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,381 @@
|
|
|
1
|
+
## 0.12.77
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 3df4f95: Scaffold virtual user runs as RPCs, backed by a run store.
|
|
6
|
+
|
|
7
|
+
`pikku persona run` could already turn a declared persona loose on a running
|
|
8
|
+
stage, but only from a terminal, and the result only existed in that terminal's
|
|
9
|
+
output. There was no way for CI, a console, or a scheduled job to start a run —
|
|
10
|
+
and nothing kept what a run found, so this week's findings could not be compared
|
|
11
|
+
against last week's.
|
|
12
|
+
|
|
13
|
+
`scaffold.virtualUser` now generates two RPCs and the function behind them:
|
|
14
|
+
- `runVirtualUser({ persona, goals?, memory?, disposition?, budget?, seed? })
|
|
15
|
+
-> { runId }`
|
|
16
|
+
- `getVirtualUserRun({ runId }) -> { status, findings, tally, memory, … }`
|
|
17
|
+
|
|
18
|
+
They are gated on separate scopes — `virtualUser:run` and `virtualUser:read` —
|
|
19
|
+
because an adversarial run's findings are working exploits carrying live ids,
|
|
20
|
+
which makes reading them the more sensitive of the two. Production refuses every
|
|
21
|
+
disposition but `accountable`, checked against the effective one so the
|
|
22
|
+
per-run override cannot smuggle another in.
|
|
23
|
+
|
|
24
|
+
**A run is not a workflow and not a queued job.** It explores, so no two
|
|
25
|
+
attempts take the same steps and there is nothing to replay; and the record
|
|
26
|
+
already carries the progress a queue would only be holding on the way to the
|
|
27
|
+
same place. `runVirtualUser` writes the record, dispatches without awaiting, and
|
|
28
|
+
returns the id. The cost is stated on the type: a restart mid-run strands a
|
|
29
|
+
record at `running`, so a run older than its budget window and still `running`
|
|
30
|
+
is dead rather than working.
|
|
31
|
+
|
|
32
|
+
`@pikku/core` gains `VirtualUserRunStore` (with `virtualUserRunStore` on
|
|
33
|
+
`CoreSingletonServices`), and `@pikku/kysely` ships
|
|
34
|
+
`KyselyVirtualUserRunStore`, which creates its own table on first use like the
|
|
35
|
+
audit sink — the runtime never needs it, so it arrives with the feature that
|
|
36
|
+
fills it rather than in every database.
|
|
37
|
+
|
|
38
|
+
Also in core: `prepareVirtualUserRun`, which derives the catalogue, intents,
|
|
39
|
+
scopes and reachable agents in one place. `pikku persona run` reads the
|
|
40
|
+
inspector state and the generated RPC reads `metaService`, and the two have to
|
|
41
|
+
agree — otherwise the same persona and seed explore a different API depending on
|
|
42
|
+
how the run was started. `personaScopes` moved here from the CLI for the same
|
|
43
|
+
reason and is still re-exported from its old home. `PRODUCTION_DISPOSITION` is
|
|
44
|
+
now exported from `@pikku/core/virtual-user`, which it should always have been.
|
|
45
|
+
|
|
46
|
+
## 0.12.76
|
|
47
|
+
|
|
48
|
+
### Patch Changes
|
|
49
|
+
|
|
50
|
+
- 62ea4cc: The audit trail is now readable — in the generated meta, through an RPC, and as
|
|
51
|
+
a page in the console.
|
|
52
|
+
|
|
53
|
+
`audit: true` reaches `FunctionRuntimeMeta.audit` as its resolved form
|
|
54
|
+
(`{ durability }`), so which functions record anything is answerable without
|
|
55
|
+
running them. It is informational: the runner still resolves audit from the live
|
|
56
|
+
function config, so meta and runtime cannot disagree.
|
|
57
|
+
|
|
58
|
+
`AuditService` grows an optional read side — `query(AuditQuery)` and `facets()`.
|
|
59
|
+
Optional because a sink can legitimately be write-only: a queue producer that
|
|
60
|
+
hands events to another system has nothing to read back, and a reader that finds
|
|
61
|
+
these absent should say the trail is not readable here rather than that it is
|
|
62
|
+
empty. The two are very different answers to give someone auditing a system.
|
|
63
|
+
|
|
64
|
+
`KyselyAuditService` implements both, newest first with offset paging, filtered
|
|
65
|
+
by user, action and time window. Two things it now gets right that are easy to
|
|
66
|
+
get wrong: an empty filter array means "match nothing" rather than "no filter",
|
|
67
|
+
and results are read by physical _and_ camelCase key, because `CamelCasePlugin`
|
|
68
|
+
is on most pikku Kysely instances and renames result keys on the way out — the
|
|
69
|
+
mismatch does not throw, it returns a page of `undefined`. `init()` creates the
|
|
70
|
+
`audit` table for projects that do not migrate it themselves, from a new
|
|
71
|
+
exported `auditSchema` that stays out of `pikkuSchemas` because the runtime does
|
|
72
|
+
not need it.
|
|
73
|
+
|
|
74
|
+
The console addon exposes `console:getAudits` and `console:getAuditFilters`
|
|
75
|
+
behind a new `pikku:audit:read` scope, and forwards the application's `audit`
|
|
76
|
+
service into the addon's own services — without that last part every install
|
|
77
|
+
reported the trail as unreadable, whatever sink it had configured.
|
|
78
|
+
|
|
79
|
+
The console gets an Audit trail page: an infinite list filtered server-side by
|
|
80
|
+
user and action, and a row that opens the whole event, metadata rendered as a
|
|
81
|
+
JSON tree. Refused, unreadable and empty are three different screens, because
|
|
82
|
+
"you may not read this", "nobody can read this" and "nothing happened" are three
|
|
83
|
+
different facts.
|
|
84
|
+
|
|
85
|
+
Events name the person who caused them. The trail records a user id — the only
|
|
86
|
+
thing stable enough to record, since a name can change after the event — so
|
|
87
|
+
`getAudits` resolves those ids against better-auth's user directory at read
|
|
88
|
+
time, and the page shows the name while keeping the recorded id on the event.
|
|
89
|
+
The filter follows: pick a colleague by name, filter by the id. A scenario
|
|
90
|
+
actor is labelled as one, so synthetic traffic is not mistaken for real, and a
|
|
91
|
+
caller who was signed out shows the wire identity pikku resolved for them
|
|
92
|
+
rather than being credited to the system.
|
|
93
|
+
|
|
94
|
+
**Breaking, for anyone already reading `AuditEvent`:** `actor` is now
|
|
95
|
+
`userIdentity`, and its type `AuditActor` is `AuditUserIdentity`; `AuditQuery`
|
|
96
|
+
takes `userIds`/`orgId` in place of `actorUserIds`/`actorOrgId`, and
|
|
97
|
+
`AuditFacets` returns `userIds`. In pikku an _actor_ is a synthetic person a
|
|
98
|
+
scenario drives, flagged on the user row — so naming the causer of an event
|
|
99
|
+
`actor` made the synthetic case unsayable (`actor.actor === true`) and implied
|
|
100
|
+
every recorded action was a test. The overwhelming majority are ordinary
|
|
101
|
+
customers. The `audit` table follows: `actor_user_id` / `actor_org_id`
|
|
102
|
+
are now `user_id` / `org_id`, and a `pikku_user_id` column joins them so the
|
|
103
|
+
wire identity of a caller who never signed in survives the round trip — the
|
|
104
|
+
sink was dropping it, which left the console's Session field permanently
|
|
105
|
+
blank. A project that already migrated the table needs to rename the two
|
|
106
|
+
columns and add the third; `KyselyAuditService.init()` creates the new shape
|
|
107
|
+
for anyone who did not.
|
|
108
|
+
|
|
109
|
+
- 9dddff8: Split a column's at-rest form out of its classification.
|
|
110
|
+
|
|
111
|
+
`security: 'encrypted'` sat beside `'secret'` as though the two were
|
|
112
|
+
alternatives, which made the field unanswerable: a token hash and a live bearer
|
|
113
|
+
token are both secret, one must never be encrypted — the digest _is_ the lookup
|
|
114
|
+
key — and the other must always be. A column now carries a second, independent
|
|
115
|
+
`form: 'plain' | 'hashed' | 'wrapped' | 'sealed'` saying how the bytes are held.
|
|
116
|
+
|
|
117
|
+
Declaring a form other than `plain` makes the column's INSERT/UPDATE type
|
|
118
|
+
nominal — `WrappedValue`, `SealedValue`, `HashedValue` — so a plain string no
|
|
119
|
+
longer compiles there and the only way to write the column is with something an
|
|
120
|
+
encrypt, seal or hash call produced. `envelopeEncrypt`, `envelopeRewrap` and
|
|
121
|
+
`wrapDEK` now return the brand, and a new `hashToken` produces `HashedValue`, so
|
|
122
|
+
the round trip needs no casts; `column-form.ts` exports deliberately-named
|
|
123
|
+
`unsafeAs*` assertions for backfills, fixtures and values sealed elsewhere.
|
|
124
|
+
Reads are unaffected — the brands widen to `string` and compose with the
|
|
125
|
+
classification brand as `Secret<WrappedValue>`.
|
|
126
|
+
|
|
127
|
+
`wrapped` and `sealed` stay distinct because a sealed value is one the
|
|
128
|
+
application cannot read back; storing one where the other belongs is a row
|
|
129
|
+
nobody can open.
|
|
130
|
+
|
|
131
|
+
A `secret` column that has not declared a form now warns (PKU483), and a form on
|
|
132
|
+
a non-text column warns and is dropped (PKU484). Both are warnings, so existing
|
|
133
|
+
projects keep migrating — `pikku db --fail-on-warn` opts into the ratchet, and
|
|
134
|
+
an explicit `form: 'plain'` is the acknowledgement that silences it. The legacy
|
|
135
|
+
`security: 'encrypted'` keeps working and now expands to the pair it always
|
|
136
|
+
meant, `secret` + `wrapped`.
|
|
137
|
+
|
|
138
|
+
- 78b29f0: `SecretService` now returns a `SecretValue<T>` rather than the bare value, so a
|
|
139
|
+
vault secret cannot reach a sink by accident.
|
|
140
|
+
|
|
141
|
+
`SecretValue` is nominally typed, which means it is not assignable to `string`
|
|
142
|
+
(or to any other concretely-typed field). Every sink with a real type — a
|
|
143
|
+
database column, an email body, a session payload — rejects it with no lint
|
|
144
|
+
rule involved. The sinks typed `any`, `unknown`, or a free generic — the logger,
|
|
145
|
+
queue payloads, webhook and email inputs, and a function's own output — are
|
|
146
|
+
guarded with `Safe<T>`, which collapses a `SecretValue` found anywhere inside
|
|
147
|
+
`T`, however deeply nested, to `never`.
|
|
148
|
+
|
|
149
|
+
Unwrap deliberately at the point the secret reaches the wire:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
const secret = await secrets.getSecret('BETTER_AUTH_SECRET')
|
|
153
|
+
betterAuth({ secret: secret.reveal() })
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Two behaviours cover what types cannot see. Structured serialization redacts —
|
|
157
|
+
`JSON.stringify` and node's inspect both yield `[secret]`, so an audit or log
|
|
158
|
+
write stays honest without crashing the request. String coercion throws
|
|
159
|
+
`SecretCoercionError`, because a template literal is always a leak.
|
|
160
|
+
|
|
161
|
+
`AuditLog.write` is guarded the same way as the logger, since an audit event
|
|
162
|
+
carries `input` and `metadata` as `unknown` and nominality alone cannot stop a
|
|
163
|
+
secret landing in one.
|
|
164
|
+
|
|
165
|
+
`.reveal()` is the deliberate escape hatch, and what it hands back is an
|
|
166
|
+
ordinary string as far as every sink signature is concerned. **PKU953** closes
|
|
167
|
+
that gap: under `pikku all --security` the inspector reports a revealed secret
|
|
168
|
+
that flows into a logger, an audit, a queue, an email or a webhook — `console` included.
|
|
169
|
+
|
|
170
|
+
This also fixed a real one: `remote-addon-auth.ts` called `String(token)` on an
|
|
171
|
+
`unknown` and wrote the result straight into an `Authorization` header.
|
|
172
|
+
|
|
173
|
+
## 0.12.75
|
|
174
|
+
|
|
175
|
+
### Patch Changes
|
|
176
|
+
|
|
177
|
+
- 32277d5: Make a voice conversation with an agent something a chat surface can turn on, rather than
|
|
178
|
+
something each consumer reassembles.
|
|
179
|
+
|
|
180
|
+
The server half already worked — `voiceInput` transcribed, `voiceOutput` synthesized a
|
|
181
|
+
sentence at a time, and the AG-UI mapper forwarded the audio. What was missing was the
|
|
182
|
+
turn's own words. The client sends audio, so only the server ever knows what was said,
|
|
183
|
+
and nothing carried that back: a spoken turn rendered as an empty user bubble followed by
|
|
184
|
+
an answer to a question nobody could see, and thread history recorded the base64 audio
|
|
185
|
+
blob instead of the transcript — megabytes of unreadable data in place of the only
|
|
186
|
+
readable record of the turn.
|
|
187
|
+
|
|
188
|
+
`voiceInput` now records what it heard, the stream emits it as a `transcript` event ahead
|
|
189
|
+
of the run (the reply starts within a few hundred milliseconds, and a question that
|
|
190
|
+
appears after its answer reads as the wrong question), and it reaches the browser as
|
|
191
|
+
`pikku:transcript`. Both run paths persist the transcribed message rather than the one
|
|
192
|
+
that arrived on the wire. `audio-delta` also carries the sentence it says, which is what
|
|
193
|
+
a barge-in needs to report the part the user actually heard — a reply cut off after "I'll
|
|
194
|
+
delete the staging database and" is answered very differently depending on whether the
|
|
195
|
+
model knows the sentence never landed.
|
|
196
|
+
|
|
197
|
+
`@pikku/voice-agents` gains the two things a voice UI needs and could not get: a live
|
|
198
|
+
input level, attached to the source rather than to a detector so it keeps reading on the
|
|
199
|
+
Silero path, and the microphone list — re-readable on demand, because device labels are
|
|
200
|
+
empty until permission is granted and nothing fires when it is. `VoiceSession` also
|
|
201
|
+
learned manual turn boundaries, so push-to-talk is a mode rather than a detector fought
|
|
202
|
+
to a standstill: holding the key through a three-second pause is someone thinking, and
|
|
203
|
+
any endpointer worth having would cut them off.
|
|
204
|
+
|
|
205
|
+
`<PikkuAgentChat voice />` puts a microphone beside the send button, promotes it to
|
|
206
|
+
primary when nothing is typed, and opens an indicator with a live level bar, a device
|
|
207
|
+
picker and a hold-to-record toggle. It plays the agent's speech, and cancels the run on
|
|
208
|
+
barge-in — talking over the agent should stop the bill, not just the sound.
|
|
209
|
+
|
|
210
|
+
Opt-in, because the component cannot check the two things it depends on: the agent has to
|
|
211
|
+
be wired with `voiceInput` for the audio to be understood and `voiceOutput` for anything
|
|
212
|
+
to come back.
|
|
213
|
+
|
|
214
|
+
- ea8aabf: Serve `LocalContent` uploads and signed reads under Bun.
|
|
215
|
+
|
|
216
|
+
`LocalContent` hands the browser a `PUT <uploadUrlPrefix>/<key>` upload URL and a signed
|
|
217
|
+
`GET <assetUrlPrefix>/<key>` read URL, but it is a `ContentService` and cannot answer
|
|
218
|
+
either — something in the serving path has to. Only `@pikku/node-http-server` did. The
|
|
219
|
+
same project served under Bun handed out upload URLs that 404ed, with nothing naming the
|
|
220
|
+
cause: the config was accepted, the service was constructed, and the URLs looked right.
|
|
221
|
+
|
|
222
|
+
`@pikku/core` now exports `createLocalContentRequestHandler` from
|
|
223
|
+
`@pikku/core/services/local-content-request-handler` — the server half of `LocalContent`,
|
|
224
|
+
expressed in Web `Request`/`Response` so every runtime shares one implementation of the
|
|
225
|
+
signature check rather than each re-deriving it. It returns `null` for anything that is
|
|
226
|
+
not a content request, which is the caller's signal to carry on with its normal routing.
|
|
227
|
+
|
|
228
|
+
`PikkuBunServer` accepts `config.content` and a `contentSigningJWT` option, mirroring
|
|
229
|
+
`PikkuNodeHTTPServer`, and answers both prefixes ahead of static mounts and routing.
|
|
230
|
+
`BunServerRunner` was dropping `contentSigningJWT` on the floor, which silently disabled
|
|
231
|
+
signed asset reads for every Bun project even once the prefixes were served — the config
|
|
232
|
+
arrived, the service that verifies its signatures did not.
|
|
233
|
+
|
|
234
|
+
Signed reads are refused unless every claim matches, the path included: without that, a
|
|
235
|
+
signature minted for one asset would read any other.
|
|
236
|
+
|
|
237
|
+
- 33e96ab: Make a CLI served over a channel typecheck in a real project.
|
|
238
|
+
|
|
239
|
+
Both of these are unreachable for a hand-written `wireChannel`, whose routes are usually
|
|
240
|
+
bare identifiers, and unavoidable for a CLI one, whose routes are command ids.
|
|
241
|
+
|
|
242
|
+
`ChannelsMap` emitted route and message keys unquoted. A command id is a kebab or dotted
|
|
243
|
+
name far more often than not — `app-smoke`, `registry.search`, `package.upgrade-pikku` —
|
|
244
|
+
and each one ends the property early, so the generated map is not parseable TypeScript at
|
|
245
|
+
all. One project's map came out with 107 syntax errors from a single CLI channel. Keys are
|
|
246
|
+
now quoted when they are not bare identifiers, and left alone when they are, so existing
|
|
247
|
+
generated output is unchanged.
|
|
248
|
+
|
|
249
|
+
`executeRawCLIViaChannel` typed its renderers `Record<string, CorePikkuCLIRender<any>>`,
|
|
250
|
+
whose services parameter defaults to `CoreServices`. The renderers a generated client
|
|
251
|
+
passes are the app's own, typed against its `SingletonServices`, and a function taking
|
|
252
|
+
those is not assignable to one taking `CoreServices` — so the generated client failed to
|
|
253
|
+
compile for any app that adds a service, which is every app.
|
|
254
|
+
|
|
255
|
+
Rather than widen the type, it now says what is actually true on that side of the socket:
|
|
256
|
+
a renderer running on the client gets a logger and nothing else, because there is no
|
|
257
|
+
service container there to resolve anything from. `CorePikkuCLIClientRender` and
|
|
258
|
+
`ClientCLIRenderServices` are new exports of `@pikku/core/cli/channel`. They are not
|
|
259
|
+
expressible as `CorePikkuCLIRender`, whose `Services` parameter is constrained to
|
|
260
|
+
`CoreSingletonServices` and so demands a `config`, `variables` and `secrets` the client
|
|
261
|
+
cannot invent. The one cast from the app's renderer type to that shape is localised to the
|
|
262
|
+
generated client, where it is sound: generation refuses to emit the file at all if a
|
|
263
|
+
renderer reaches for a service other than `logger`.
|
|
264
|
+
|
|
265
|
+
- fd72e58: Drop `scenario.step` — a scenario step is now always a `given`, `when` or
|
|
266
|
+
`then`.
|
|
267
|
+
|
|
268
|
+
`step` rendered no keyword, which made it the phase to reach for whenever a
|
|
269
|
+
step did not obviously fit one of the three. That is exactly the step a reader
|
|
270
|
+
cannot check: a scenario is read by people deciding whether it describes the
|
|
271
|
+
behaviour they wanted, and a row that says what it does without saying whether
|
|
272
|
+
it is setup, action or claim tells them nothing to agree or disagree with. It
|
|
273
|
+
was also the escape hatch from the assertion lint — a scenario with no `then`
|
|
274
|
+
could be made to stop complaining by demoting its steps rather than by
|
|
275
|
+
asserting anything.
|
|
276
|
+
|
|
277
|
+
Replace `scenario.step(...)` with whichever of `given`, `when` or `then` the
|
|
278
|
+
step actually is. `then` is not a rename: it makes the step's bindings
|
|
279
|
+
witnesses rather than alternatives, so every declared surface runs and they
|
|
280
|
+
must agree.
|
|
281
|
+
|
|
282
|
+
- fd72e58: Make personas a first-class surface rather than a detail of the test runner.
|
|
283
|
+
|
|
284
|
+
A persona is now read in three places — the knowledge base resolves `persona:`
|
|
285
|
+
URIs against it, scenarios cast it as an actor, and a virtual user runs as it —
|
|
286
|
+
so it gets its own page at `/personas` under a new **People** section in the
|
|
287
|
+
rail, alongside Users. The card is a profile: avatar, name, job title, computed
|
|
288
|
+
address, the system roles they hold, and how many scenarios cast them. Opening
|
|
289
|
+
one expands each role to the scopes it confers, which is the half of the picture
|
|
290
|
+
that explains a 403.
|
|
291
|
+
|
|
292
|
+
`definePersonas` takes an optional `avatarUrl` — any URL a browser can load.
|
|
293
|
+
Nothing is derived from the address: a persona's address is synthetic, so a
|
|
294
|
+
derived identicon would be the same shrug for everyone. Omitted, the console
|
|
295
|
+
keeps drawing the deterministic colour-and-icon avatar from the persona's id.
|
|
296
|
+
|
|
297
|
+
- fd72e58: Read the actors that are not people on the personas page.
|
|
298
|
+
|
|
299
|
+
The platform — the app acting on itself, what `pikkuPlatformScenarioStep`
|
|
300
|
+
declares — now has a row of its own, alongside one per addon whose system a
|
|
301
|
+
step makes act. They sit behind a People / System / All filter that opens on
|
|
302
|
+
the people: a subject holds no roles and signs in as nobody, so leading with it
|
|
303
|
+
would put the rows nothing is authorized through above the ones that are.
|
|
304
|
+
|
|
305
|
+
The platform row is built in rather than derived. A project that has never
|
|
306
|
+
written a platform step still has a platform, and a card that appeared the
|
|
307
|
+
moment somebody declared their first step would read as a feature they had
|
|
308
|
+
switched on.
|
|
309
|
+
|
|
310
|
+
Also: PKU680 now counts `expectService`, `expectError` and `expectEventually`
|
|
311
|
+
as assertions. They are inline steps and carry no phase, so a scenario whose
|
|
312
|
+
only witness was a recorded service call was being told it never asserts.
|
|
313
|
+
|
|
314
|
+
- 894b2f8: `defineScope` and `defineSystemRole` accumulate across call sites again. Only `definePersonas` is one-per-codebase.
|
|
315
|
+
|
|
316
|
+
The previous release made all three single-declaration constructs, which no project scaffolding user-admin could satisfy: the CLI generates a `defineScope` of its own in `user-admin.gen.ts` carrying the whole `admin` tree, and `@pikku/addon-console` spells the same tree out again, so a second hand-written declaration failed the build with PKU583 — and the losing file's scopes were dropped from the metadata rather than merged.
|
|
317
|
+
|
|
318
|
+
Exempting generated files would have reinstated exactly the ambiguity the rule removes, only for the files nobody can read the rule from. The real fix is for `admin` to be a default scope nobody declares, at which point the rule can come back for scopes and roles.
|
|
319
|
+
|
|
320
|
+
`definePersonas` is unaffected: nothing generates one, so its single call site stands.
|
|
321
|
+
|
|
322
|
+
- dd19aa7: Drop `scopes` from sessionless functions, rename `selfAuthenticated`, and make both
|
|
323
|
+
escape hatches opt-in.
|
|
324
|
+
|
|
325
|
+
**`scopes` are gone from `pikkuSessionlessFunc`.** They are AND-ed and `verifyScopes`
|
|
326
|
+
fails closed on a session that does not exist, so every scope listed on a sessionless
|
|
327
|
+
function rejected the anonymous caller it exists to serve. `CorePikkuSessionlessFunctionConfig`
|
|
328
|
+
now states this once in core, and the generated `pikkuSessionlessFunc` / `pikkuVoidFunc`
|
|
329
|
+
configs derive from it — so the field is absent rather than subtracted.
|
|
330
|
+
|
|
331
|
+
`@pikku/addon-console`'s `installAddon` and `installOpenapiAddon` are now `pikkuFunc`.
|
|
332
|
+
Both set `auth: true` and `scopes: ['admin']`, and a test exercises that gate, so the
|
|
333
|
+
scopes were load-bearing — they only compiled as sessionless because the config accepted
|
|
334
|
+
a field it could not honour. No behaviour change: both already required a session.
|
|
335
|
+
|
|
336
|
+
**`selfAuthenticated` is now `permissionsInBody`.** It never described authentication:
|
|
337
|
+
what it records is that the permission check lives in the function body rather than in a
|
|
338
|
+
declared `permissions` entry.
|
|
339
|
+
|
|
340
|
+
**Both escape hatches must be opted into**, via a new `allow` block in
|
|
341
|
+
`pikku.config.json`:
|
|
342
|
+
|
|
343
|
+
```json
|
|
344
|
+
"allow": { "permissionsInBody": true, "complexWorkflows": true }
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Unset means unavailable, and using the feature is a build error naming the flag that
|
|
348
|
+
would permit it — PKU576 for `permissionsInBody`, PKU643 for `pikkuWorkflowComplexFunc`.
|
|
349
|
+
Both trade something the tooling can inspect for something only a reader can verify: a
|
|
350
|
+
permission check buried in a body, or workflow steps that cannot be serialized into the
|
|
351
|
+
graph, replayed, or migrated. Both are occasionally right, and both are the path of least
|
|
352
|
+
resistance whenever the declarative form is merely inconvenient. Whoever owns the project
|
|
353
|
+
makes that call once, in writing, instead of every author making it silently at the call
|
|
354
|
+
site.
|
|
355
|
+
|
|
356
|
+
**PKU574's message no longer contradicts any of this.** Every function it reports is
|
|
357
|
+
sessionless — that is how the population is selected, not a finding — yet it opened by
|
|
358
|
+
reporting that they "require neither a session", then advised adding scopes. It now names
|
|
359
|
+
them as sessionless and recommends only gates an anonymous caller can meet:
|
|
360
|
+
`permissions`, `auth: true`, `wireAddon({ auth: true })`, or dropping `expose: true`.
|
|
361
|
+
`permissionsInBody` is deliberately absent from that list: a diagnostic should not
|
|
362
|
+
advertise its own escape hatch.
|
|
363
|
+
|
|
364
|
+
- 50ec500: Make `defineScope`, `defineSystemRole` and `definePersonas` single-declaration constructs
|
|
365
|
+
— exactly one call site per codebase, the rule `pikkuBetterAuth` has always had.
|
|
366
|
+
|
|
367
|
+
Each of the three already takes a keyed object, so one call declares as many entries as
|
|
368
|
+
you like. Spreading the calls across files bought nothing and cost the thing that matters:
|
|
369
|
+
there was no answer to "where do I add a persona?", so downstream tooling and agents had
|
|
370
|
+
nowhere unambiguous to read from or append to. The only duplicate handling that existed
|
|
371
|
+
caught a narrow case — the same id declared twice with different content — and said
|
|
372
|
+
nothing about the same id declared twice in two files.
|
|
373
|
+
|
|
374
|
+
A second call now fails the build with `PKU583` (`defineScope`), `PKU584`
|
|
375
|
+
(`defineSystemRole`) or `PKU585` (`definePersonas`), naming both source files and saying
|
|
376
|
+
to declare them all in one call. A second call in the _same_ file is refused too: "the
|
|
377
|
+
file" is not an answer either when the file holds two calls.
|
|
378
|
+
|
|
1
379
|
## 0.12.74
|
|
2
380
|
|
|
3
381
|
### Patch Changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { HashedValue, SealedValue, WrappedValue } from './data-classification.js';
|
|
2
|
+
/**
|
|
3
|
+
* SHA-256 of a credential, lowercase hex — the sole producer of `HashedValue`.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately unpeppered. These are high-entropy random bearer tokens, not
|
|
6
|
+
* passwords: there is no candidate set to search, so a pepper would add a key
|
|
7
|
+
* to manage and a rotation story to own while buying nothing. A column holding
|
|
8
|
+
* a *low*-entropy secret is not a `hashed` column — it wants a password KDF and
|
|
9
|
+
* a different type entirely.
|
|
10
|
+
*
|
|
11
|
+
* Async because it runs on WebCrypto rather than `node:crypto`, so the same
|
|
12
|
+
* code path works in a Worker. Callers that hash a bearer token on every
|
|
13
|
+
* request are already async.
|
|
14
|
+
*/
|
|
15
|
+
export declare const hashToken: (raw: string) => Promise<HashedValue>;
|
|
16
|
+
/**
|
|
17
|
+
* Assert that a string already in storage is ciphertext of the given form.
|
|
18
|
+
*
|
|
19
|
+
* The brands exist so that new writes must come from a real encrypt/hash call,
|
|
20
|
+
* but three paths legitimately hold such a value as a bare `string`: a
|
|
21
|
+
* migration backfilling rows written before the column declared its form, a
|
|
22
|
+
* test fixture, and a value arriving over the wire from a service that sealed
|
|
23
|
+
* it elsewhere. Each of those is a promise the caller is making, not something
|
|
24
|
+
* the type system can check — which is why these are named to be greppable and
|
|
25
|
+
* why there is no non-`unsafe` spelling. If one of these appears in ordinary
|
|
26
|
+
* request-handling code, that is the bug.
|
|
27
|
+
*/
|
|
28
|
+
export declare const unsafeAsWrapped: (stored: string) => WrappedValue;
|
|
29
|
+
/** See `unsafeAsWrapped`. */
|
|
30
|
+
export declare const unsafeAsSealed: (stored: string) => SealedValue;
|
|
31
|
+
/** See `unsafeAsWrapped`. */
|
|
32
|
+
export declare const unsafeAsHashed: (stored: string) => HashedValue;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHA-256 of a credential, lowercase hex — the sole producer of `HashedValue`.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately unpeppered. These are high-entropy random bearer tokens, not
|
|
5
|
+
* passwords: there is no candidate set to search, so a pepper would add a key
|
|
6
|
+
* to manage and a rotation story to own while buying nothing. A column holding
|
|
7
|
+
* a *low*-entropy secret is not a `hashed` column — it wants a password KDF and
|
|
8
|
+
* a different type entirely.
|
|
9
|
+
*
|
|
10
|
+
* Async because it runs on WebCrypto rather than `node:crypto`, so the same
|
|
11
|
+
* code path works in a Worker. Callers that hash a bearer token on every
|
|
12
|
+
* request are already async.
|
|
13
|
+
*/
|
|
14
|
+
export const hashToken = async (raw) => {
|
|
15
|
+
const subtle = globalThis.crypto?.subtle;
|
|
16
|
+
if (!subtle) {
|
|
17
|
+
throw new Error('WebCrypto not available');
|
|
18
|
+
}
|
|
19
|
+
const digest = await subtle.digest('SHA-256', new TextEncoder().encode(raw));
|
|
20
|
+
let hex = '';
|
|
21
|
+
for (const byte of new Uint8Array(digest)) {
|
|
22
|
+
hex += byte.toString(16).padStart(2, '0');
|
|
23
|
+
}
|
|
24
|
+
return hex;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Assert that a string already in storage is ciphertext of the given form.
|
|
28
|
+
*
|
|
29
|
+
* The brands exist so that new writes must come from a real encrypt/hash call,
|
|
30
|
+
* but three paths legitimately hold such a value as a bare `string`: a
|
|
31
|
+
* migration backfilling rows written before the column declared its form, a
|
|
32
|
+
* test fixture, and a value arriving over the wire from a service that sealed
|
|
33
|
+
* it elsewhere. Each of those is a promise the caller is making, not something
|
|
34
|
+
* the type system can check — which is why these are named to be greppable and
|
|
35
|
+
* why there is no non-`unsafe` spelling. If one of these appears in ordinary
|
|
36
|
+
* request-handling code, that is the bug.
|
|
37
|
+
*/
|
|
38
|
+
export const unsafeAsWrapped = (stored) => stored;
|
|
39
|
+
/** See `unsafeAsWrapped`. */
|
|
40
|
+
export const unsafeAsSealed = (stored) => stored;
|
|
41
|
+
/** See `unsafeAsWrapped`. */
|
|
42
|
+
export const unsafeAsHashed = (stored) => stored;
|
package/dist/crypto-utils.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WrappedValue } from './data-classification.js';
|
|
1
2
|
export declare const encryptJSON: (secret: string, value: unknown) => Promise<string>;
|
|
2
3
|
export declare const decryptJSON: <T>(secret: string, token: string) => Promise<T>;
|
|
3
4
|
/**
|
|
@@ -26,12 +27,22 @@ export declare const generateDEK: () => Promise<string>;
|
|
|
26
27
|
*/
|
|
27
28
|
export declare const generateKEKSalt: () => string;
|
|
28
29
|
export declare const deriveKEK: (passphrase: string, salt: string) => Promise<CryptoKey>;
|
|
29
|
-
export declare const wrapDEK: (kek: CryptoKey, plaintextDEK: string) => Promise<
|
|
30
|
+
export declare const wrapDEK: (kek: CryptoKey, plaintextDEK: string) => Promise<WrappedValue>;
|
|
30
31
|
export declare const unwrapDEK: (kek: CryptoKey, wrappedDEK: string) => Promise<string>;
|
|
31
32
|
export interface EnvelopeEncryptResult {
|
|
32
|
-
ciphertext:
|
|
33
|
-
wrappedDEK:
|
|
33
|
+
ciphertext: WrappedValue;
|
|
34
|
+
wrappedDEK: WrappedValue;
|
|
34
35
|
}
|
|
35
36
|
export declare const envelopeEncrypt: (kek: CryptoKey, value: unknown) => Promise<EnvelopeEncryptResult>;
|
|
37
|
+
/**
|
|
38
|
+
* Note the inputs are plain `string`, not `WrappedValue`.
|
|
39
|
+
*
|
|
40
|
+
* The brand exists to stop plaintext being *written* to a wrapped column, and
|
|
41
|
+
* a `WrappedValue` is assignable to `string`, so a branded caller still passes
|
|
42
|
+
* without a cast. Demanding the brand here would buy nothing — feeding the
|
|
43
|
+
* wrong string in already fails at the AEAD tag — while forcing a cast into
|
|
44
|
+
* every path that reads ciphertext back out of a row, a parsed envelope, or the
|
|
45
|
+
* wire, which is exactly where casts are least reviewable.
|
|
46
|
+
*/
|
|
36
47
|
export declare const envelopeDecrypt: <T>(kek: CryptoKey, ciphertext: string, wrappedDEK: string) => Promise<T>;
|
|
37
|
-
export declare const envelopeRewrap: (oldKEK: CryptoKey, newKEK: CryptoKey, wrappedDEK: string) => Promise<
|
|
48
|
+
export declare const envelopeRewrap: (oldKEK: CryptoKey, newKEK: CryptoKey, wrappedDEK: string) => Promise<WrappedValue>;
|
package/dist/crypto-utils.js
CHANGED
|
@@ -217,8 +217,14 @@ export const generateKEKSalt = () => {
|
|
|
217
217
|
export const deriveKEK = async (passphrase, salt) => {
|
|
218
218
|
return deriveKey(passphrase, fromBase64Url(salt));
|
|
219
219
|
};
|
|
220
|
+
/**
|
|
221
|
+
* The one place a `WrappedValue` is minted. Every branded return below routes
|
|
222
|
+
* through here, so the assertion "these bytes really are ciphertext" is made
|
|
223
|
+
* once and audited once instead of at each call site.
|
|
224
|
+
*/
|
|
225
|
+
const asWrapped = (ciphertext) => ciphertext;
|
|
220
226
|
export const wrapDEK = async (kek, plaintextDEK) => {
|
|
221
|
-
return encryptWithCryptoKey(kek, plaintextDEK);
|
|
227
|
+
return asWrapped(await encryptWithCryptoKey(kek, plaintextDEK));
|
|
222
228
|
};
|
|
223
229
|
export const unwrapDEK = async (kek, wrappedDEK) => {
|
|
224
230
|
return decryptWithCryptoKey(kek, wrappedDEK);
|
|
@@ -231,10 +237,20 @@ const decryptWithDEK = async (dekBase64, token) => {
|
|
|
231
237
|
};
|
|
232
238
|
export const envelopeEncrypt = async (kek, value) => {
|
|
233
239
|
const dek = await generateDEK();
|
|
234
|
-
const ciphertext = await encryptWithDEK(dek, value);
|
|
240
|
+
const ciphertext = asWrapped(await encryptWithDEK(dek, value));
|
|
235
241
|
const wrappedDEK = await wrapDEK(kek, dek);
|
|
236
242
|
return { ciphertext, wrappedDEK };
|
|
237
243
|
};
|
|
244
|
+
/**
|
|
245
|
+
* Note the inputs are plain `string`, not `WrappedValue`.
|
|
246
|
+
*
|
|
247
|
+
* The brand exists to stop plaintext being *written* to a wrapped column, and
|
|
248
|
+
* a `WrappedValue` is assignable to `string`, so a branded caller still passes
|
|
249
|
+
* without a cast. Demanding the brand here would buy nothing — feeding the
|
|
250
|
+
* wrong string in already fails at the AEAD tag — while forcing a cast into
|
|
251
|
+
* every path that reads ciphertext back out of a row, a parsed envelope, or the
|
|
252
|
+
* wire, which is exactly where casts are least reviewable.
|
|
253
|
+
*/
|
|
238
254
|
export const envelopeDecrypt = async (kek, ciphertext, wrappedDEK) => {
|
|
239
255
|
const dek = await unwrapDEK(kek, wrappedDEK);
|
|
240
256
|
return decryptWithDEK(dek, ciphertext);
|
|
@@ -9,12 +9,56 @@ export type Secret<T> = T & {
|
|
|
9
9
|
};
|
|
10
10
|
export type Classification = 'public' | 'private' | 'pii' | 'secret';
|
|
11
11
|
export type AnonymizeStrategy = 'fake:email' | 'fake:name' | 'hash' | 'keep' | null;
|
|
12
|
+
/**
|
|
13
|
+
* How a column's bytes are represented at rest, as distinct from how sensitive
|
|
14
|
+
* the value is (`Classification`). The two are independent: a token hash is
|
|
15
|
+
* `secret` + `hashed` and must never be encrypted, because the hash *is* the
|
|
16
|
+
* lookup key; a live bearer token is `secret` + `plain` today and should not be.
|
|
17
|
+
*
|
|
18
|
+
* `wrapped` and `sealed` are siblings rather than one being "encrypted": both
|
|
19
|
+
* are ciphertext, and what separates them is who can read it back. Wrapped is
|
|
20
|
+
* symmetric and the application holds the key. Sealed is asymmetric and the
|
|
21
|
+
* application holds only the public half, so it can write the value and never
|
|
22
|
+
* read it. Storing one where the other is expected is silent, permanent data
|
|
23
|
+
* loss, which is why they are not collapsed into a single `encrypted`.
|
|
24
|
+
*/
|
|
25
|
+
export type ColumnForm = 'plain' | 'hashed' | 'wrapped' | 'sealed';
|
|
26
|
+
declare const wrappedBrand: unique symbol;
|
|
27
|
+
declare const sealedBrand: unique symbol;
|
|
28
|
+
declare const hashedBrand: unique symbol;
|
|
29
|
+
/**
|
|
30
|
+
* Ciphertext under a symmetric key the application holds — the output of
|
|
31
|
+
* `envelopeEncrypt`, `wrapDEK` or `envelopeRewrap`.
|
|
32
|
+
*
|
|
33
|
+
* Unlike `Secret<T>` the brand is REQUIRED, so a plain `string` is not
|
|
34
|
+
* assignable and a column declared `form: 'wrapped'` cannot be written with
|
|
35
|
+
* anything but genuine ciphertext. It stays assignable *to* `string`, so it
|
|
36
|
+
* still works as a query operand and serializes normally — the constraint is on
|
|
37
|
+
* construction, not on use.
|
|
38
|
+
*/
|
|
39
|
+
export type WrappedValue = string & {
|
|
40
|
+
readonly [wrappedBrand]: true;
|
|
41
|
+
};
|
|
42
|
+
/** Ciphertext under a public key whose private half the application does not
|
|
43
|
+
* hold. Deliberately not assignable to `WrappedValue`: writing one where the
|
|
44
|
+
* other belongs produces a row nobody can ever open. */
|
|
45
|
+
export type SealedValue = string & {
|
|
46
|
+
readonly [sealedBrand]: true;
|
|
47
|
+
};
|
|
48
|
+
/** A one-way digest of a secret input. The brand's job is narrow — stop a *raw*
|
|
49
|
+
* credential being written into the column that should hold its hash. */
|
|
50
|
+
export type HashedValue = string & {
|
|
51
|
+
readonly [hashedBrand]: true;
|
|
52
|
+
};
|
|
12
53
|
export interface ColumnClassification {
|
|
13
54
|
classification: Classification;
|
|
14
55
|
anonymize_strategy: AnonymizeStrategy;
|
|
56
|
+
/** At-rest representation. Absent means `plain`. */
|
|
57
|
+
form?: ColumnForm;
|
|
15
58
|
description?: string;
|
|
16
59
|
}
|
|
17
60
|
export type ClassificationManifest = {
|
|
18
61
|
version: 1;
|
|
19
62
|
tables: Record<string, Record<string, ColumnClassification>>;
|
|
20
63
|
};
|
|
64
|
+
export {};
|
|
@@ -4,13 +4,14 @@ import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
|
4
4
|
import type { PikkuError } from '../errors/error-handler.js';
|
|
5
5
|
import type { CoreNodeConfig } from '../wirings/node/node.types.js';
|
|
6
6
|
import type { ScenarioSurface } from '../wirings/workflow/scenario-step.types.js';
|
|
7
|
+
import type { Safe } from '../secret-value.js';
|
|
7
8
|
/**
|
|
8
9
|
* @deprecated Use StandardSchemaV1 from @standard-schema/spec instead.
|
|
9
10
|
* This alias exists only for backward compatibility with generated code.
|
|
10
11
|
*/
|
|
11
12
|
export type ZodLike<T = any> = StandardSchemaV1<T, T>;
|
|
12
|
-
export type CorePikkuFunction<In, Out, Services extends CoreSecretlessSingletonServices = SecretlessServices<CoreServices>, Session extends CoreUserSession = CoreUserSession, Wire extends PikkuWire<In, Out, true, Session, PikkuRPC, null, string> = PikkuWire<In, Out, true, Session>> = (services: Services, data: In, wire: Wire) => Wire['channel'] extends null ? Promise<Out
|
|
13
|
-
export type CorePikkuFunctionSessionless<In, Out, Services extends CoreSecretlessSingletonServices = SecretlessServices<CoreServices>, Session extends CoreUserSession = CoreUserSession, Wire extends PikkuWire<In, Out, false, Session, PikkuRPC, null, string> = PikkuWire<In, Out, false, Session>> = (services: Services, data: In, wire: Wire) => Wire['channel'] extends null ? Promise<Out
|
|
13
|
+
export type CorePikkuFunction<In, Out, Services extends CoreSecretlessSingletonServices = SecretlessServices<CoreServices>, Session extends CoreUserSession = CoreUserSession, Wire extends PikkuWire<In, Out, true, Session, PikkuRPC, null, string> = PikkuWire<In, Out, true, Session>> = (services: Services, data: In, wire: Wire) => Wire['channel'] extends null ? Promise<Safe<Out>> : Promise<Safe<Out>> | Promise<void>;
|
|
14
|
+
export type CorePikkuFunctionSessionless<In, Out, Services extends CoreSecretlessSingletonServices = SecretlessServices<CoreServices>, Session extends CoreUserSession = CoreUserSession, Wire extends PikkuWire<In, Out, false, Session, PikkuRPC, null, string> = PikkuWire<In, Out, false, Session>> = (services: Services, data: In, wire: Wire) => Wire['channel'] extends null ? Promise<Safe<Out>> : Promise<Safe<Out>> | Promise<void>;
|
|
14
15
|
export type CorePikkuPermission<In = any, Services extends CoreSecretlessSingletonServices = SecretlessServices<CoreServices>, Wire extends PikkuWire<In, never, false, any, PikkuRPC, never, never> = PikkuWire<In, never, false, any, PikkuRPC, never, never>> = (services: Services, data: In, wire: Wire) => Promise<boolean>;
|
|
15
16
|
export type CorePikkuPermissionConfig<In = any, Services extends CoreSecretlessSingletonServices = SecretlessServices<CoreServices>, Wire extends PikkuWire<In, never, false, any, PikkuRPC, never, never> = PikkuWire<In, never, false, any, PikkuRPC, never, never>> = {
|
|
16
17
|
func: CorePikkuPermission<In, Services, Wire>;
|
|
@@ -49,16 +50,18 @@ export type CorePikkuFunctionConfig<PikkuFunction extends CorePikkuFunction<any,
|
|
|
49
50
|
tags?: string[];
|
|
50
51
|
expose?: boolean;
|
|
51
52
|
/**
|
|
52
|
-
* The
|
|
53
|
+
* The permission check for this function lives in its body — verifying a
|
|
53
54
|
* signed token, checking a webhook signature, matching an invite code — so
|
|
54
|
-
* it is not open despite
|
|
55
|
+
* it is not open despite declaring no session, scope or permission.
|
|
55
56
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
57
|
+
* A last resort. Prefer `permissions`, which are declared, inspectable, and
|
|
58
|
+
* reusable; reach for this only when the check cannot be expressed as one.
|
|
59
|
+
*
|
|
60
|
+
* Purely declarative — it grants nothing, and asserting it falsely disables
|
|
61
|
+
* the audit that would have caught the mistake. Requires
|
|
62
|
+
* `allow.permissionsInBody` in `pikku.config.json`.
|
|
60
63
|
*/
|
|
61
|
-
|
|
64
|
+
permissionsInBody?: boolean;
|
|
62
65
|
remote?: boolean;
|
|
63
66
|
mcp?: boolean;
|
|
64
67
|
readonly?: boolean;
|
|
@@ -103,7 +106,8 @@ export type CorePikkuFunctionConfig<PikkuFunction extends CorePikkuFunction<any,
|
|
|
103
106
|
* Scopes the session must hold; all are required (AND) and checked before
|
|
104
107
|
* `permissions`, which OR together — a scope can only narrow access.
|
|
105
108
|
* Narrowed to the generated `ScopeId` union, so an undeclared scope is a
|
|
106
|
-
* compile error.
|
|
109
|
+
* compile error. Requires a session — see
|
|
110
|
+
* {@link CorePikkuSessionlessFunctionConfig}.
|
|
107
111
|
*/
|
|
108
112
|
scopes?: Scope[];
|
|
109
113
|
permissions?: CorePermissionGroup<PikkuPermission>;
|
|
@@ -113,3 +117,12 @@ export type CorePikkuFunctionConfig<PikkuFunction extends CorePikkuFunction<any,
|
|
|
113
117
|
node?: CoreNodeConfig;
|
|
114
118
|
errors?: Array<typeof PikkuError>;
|
|
115
119
|
};
|
|
120
|
+
/**
|
|
121
|
+
* {@link CorePikkuFunctionConfig} for a function that runs without a session.
|
|
122
|
+
*
|
|
123
|
+
* Identical, minus `scopes`. Scopes are AND-ed and `verifyScopes` fails closed,
|
|
124
|
+
* so an anonymous caller holds none and satisfies none — a sessionless function
|
|
125
|
+
* with scopes rejects every caller it exists to serve. Gate it with
|
|
126
|
+
* `permissions`, which receive the optional session and may pass anonymous.
|
|
127
|
+
*/
|
|
128
|
+
export type CorePikkuSessionlessFunctionConfig<PikkuFunction extends CorePikkuFunctionSessionless<any, any, any, any, any>, PikkuPermission extends CorePikkuPermission<any, any, any> = CorePikkuPermission<any>, PikkuMiddleware extends CorePikkuMiddleware<any, any> = CorePikkuMiddleware<any, any>, InputSchema extends StandardSchemaV1 | undefined = undefined, OutputSchema extends StandardSchemaV1 | undefined = undefined> = Omit<CorePikkuFunctionConfig<PikkuFunction, PikkuPermission, PikkuMiddleware, InputSchema, OutputSchema>, 'scopes'>;
|