@pikku/skills 0.12.21 → 0.12.25
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 +125 -0
- package/dist/skills.gen.js +1 -1
- package/package.json +1 -1
- package/skills/pikku-addon/SKILL.md +10 -9
- package/skills/pikku-agent/SKILL.md +67 -316
- package/skills/pikku-agent/references/agents.md +299 -0
- package/skills/{pikku-ai-vercel/SKILL.md → pikku-agent/references/runner-vercel.md} +2 -24
- package/skills/{pikku-ai-voice/SKILL.md → pikku-agent/references/voice.md} +1 -22
- package/skills/pikku-architect/SKILL.md +264 -0
- package/skills/pikku-auth/SKILL.md +89 -0
- package/skills/{pikku-better-auth/SKILL.md → pikku-auth/references/better-auth.md} +42 -47
- package/skills/{pikku-jose/SKILL.md → pikku-auth/references/jose.md} +1 -23
- package/skills/{pikku-machine-auth/SKILL.md → pikku-auth/references/machine-auth.md} +0 -23
- package/skills/{pikku-permissions/SKILL.md → pikku-auth/references/permissions.md} +5 -24
- package/skills/{pikku-security/SKILL.md → pikku-auth/references/sessions.md} +3 -23
- package/skills/pikku-build/SKILL.md +87 -0
- package/skills/{pikku-build-app/SKILL.md → pikku-build/references/app.md} +77 -25
- package/skills/{pikku-feature/SKILL.md → pikku-build/references/feature.md} +1 -8
- package/skills/{pikku-build-app → pikku-build}/references/multi-app.md +1 -1
- package/skills/{pikku-build-platform/SKILL.md → pikku-build/references/platform.md} +22 -37
- package/skills/{pikku-template-clone/SKILL.md → pikku-build/references/post-clone.md} +0 -6
- package/skills/{pikku-build-quick/SKILL.md → pikku-build/references/quick.md} +6 -22
- package/skills/{pikku-build-app → pikku-build}/references/ship.md +7 -1
- package/skills/pikku-concepts/SKILL.md +75 -8
- package/skills/pikku-concepts/references/concept-mapping.md +8 -8
- package/skills/pikku-deploy/SKILL.md +158 -0
- package/skills/{pikku-deploy-azure/SKILL.md → pikku-deploy/references/azure.md} +18 -50
- package/skills/pikku-deploy/references/cloudflare.md +104 -0
- package/skills/pikku-deploy/references/express.md +92 -0
- package/skills/{pikku-deploy-fastify/SKILL.md → pikku-deploy/references/fastify.md} +8 -32
- package/skills/{pikku-deploy-lambda/SKILL.md → pikku-deploy/references/lambda.md} +6 -27
- package/skills/{pikku-deploy-nextjs/SKILL.md → pikku-deploy/references/nextjs.md} +9 -33
- package/skills/pikku-deploy/references/uws.md +72 -0
- package/skills/pikku-deploy/references/ws.md +75 -0
- package/skills/pikku-emails/SKILL.md +3 -2
- package/skills/pikku-fabric/SKILL.md +20 -10
- package/skills/{pikku-fabric-debug/SKILL.md → pikku-fabric/references/debugging.md} +0 -6
- package/skills/pikku-i18n/SKILL.md +60 -207
- package/skills/{pikku-paraglide/SKILL.md → pikku-i18n/references/enum-labels.md} +0 -6
- package/skills/pikku-i18n/references/messages.md +218 -0
- package/skills/{pikku-rtl/SKILL.md → pikku-i18n/references/rtl.md} +3 -9
- package/skills/pikku-knowledge/SKILL.md +14 -0
- package/skills/pikku-kysely/SKILL.md +13 -13
- package/skills/pikku-meta/SKILL.md +58 -130
- package/skills/{pikku-deps/SKILL.md → pikku-meta/references/audit.md} +1 -17
- package/skills/pikku-meta/references/meta.md +114 -0
- package/skills/{pikku-versioning/SKILL.md → pikku-meta/references/versioning.md} +0 -26
- package/skills/pikku-middleware/SKILL.md +8 -8
- package/skills/pikku-n8n-import/SKILL.md +0 -1
- package/skills/pikku-react/SKILL.md +50 -298
- package/skills/pikku-react/references/client.md +293 -0
- package/skills/{pikku-react-query/SKILL.md → pikku-react/references/react-query.md} +2 -22
- package/skills/{pikku-workflows-client/SKILL.md → pikku-react/references/workflows.md} +1 -22
- package/skills/pikku-scenario/SKILL.md +64 -49
- package/skills/pikku-scenario/references/persona-run.md +148 -0
- package/skills/pikku-service-backends/SKILL.md +154 -0
- package/skills/pikku-service-backends/references/aws.md +106 -0
- package/skills/pikku-service-backends/references/backblaze.md +57 -0
- package/skills/pikku-service-backends/references/mongodb.md +90 -0
- package/skills/pikku-service-backends/references/redis.md +75 -0
- package/skills/pikku-service-backends/references/schema.md +63 -0
- package/skills/pikku-services/SKILL.md +68 -291
- package/skills/{pikku-audit/SKILL.md → pikku-services/references/audit.md} +0 -22
- package/skills/{pikku-config/SKILL.md → pikku-services/references/config.md} +1 -25
- package/skills/{pikku-pino/SKILL.md → pikku-services/references/pino.md} +0 -20
- package/skills/pikku-services/references/services.md +272 -0
- package/skills/pikku-software-archaeology/README.md +5 -1
- package/skills/pikku-software-archaeology/SKILL.md +15 -2
- package/skills/{pikku-product-second-opinion/example/sample-report.md → pikku-software-archaeology/example/second-opinion-sample-report.md} +1 -1
- package/skills/pikku-software-archaeology/references/blueprint.schema.json +1 -1
- package/skills/pikku-software-archaeology/references/pikku-mapping.md +3 -3
- package/skills/{pikku-product-second-opinion/SKILL.md → pikku-software-archaeology/references/second-opinion.md} +4 -9
- package/skills/pikku-webhook/SKILL.md +199 -0
- package/skills/pikku-wiring/SKILL.md +180 -0
- package/skills/{pikku-websocket/SKILL.md → pikku-wiring/references/channel.md} +2 -35
- package/skills/{pikku-cli/SKILL.md → pikku-wiring/references/cli.md} +1 -33
- package/skills/{pikku-gateway-slack/SKILL.md → pikku-wiring/references/gateway-slack.md} +0 -23
- package/skills/{pikku-http/SKILL.md → pikku-wiring/references/http.md} +4 -40
- package/skills/{pikku-mcp/SKILL.md → pikku-wiring/references/mcp.md} +0 -33
- package/skills/{pikku-queue/SKILL.md → pikku-wiring/references/queue.md} +1 -33
- package/skills/{pikku-realtime/SKILL.md → pikku-wiring/references/realtime.md} +2 -25
- package/skills/{pikku-rpc/SKILL.md → pikku-wiring/references/rpc.md} +0 -32
- package/skills/{pikku-schedule/SKILL.md → pikku-wiring/references/scheduler.md} +1 -35
- package/skills/{pikku-trigger/SKILL.md → pikku-wiring/references/trigger.md} +0 -43
- package/skills/pikku-workflow/SKILL.md +3 -3
- package/skills/pikku-aws/SKILL.md +0 -161
- package/skills/pikku-backblaze/SKILL.md +0 -104
- package/skills/pikku-deploy-cloudflare/SKILL.md +0 -123
- package/skills/pikku-deploy-express/SKILL.md +0 -122
- package/skills/pikku-deploy-uws/SKILL.md +0 -144
- package/skills/pikku-mongodb/SKILL.md +0 -113
- package/skills/pikku-product-second-opinion/README.md +0 -43
- package/skills/pikku-redis/SKILL.md +0 -99
- package/skills/pikku-schema-ajv/SKILL.md +0 -83
- package/skills/pikku-schema-cfworker/SKILL.md +0 -82
- package/skills/pikku-ws/SKILL.md +0 -87
- /package/skills/{pikku-build-app → pikku-build}/references/theming.md +0 -0
- /package/skills/{pikku-product-second-opinion/references/report-template.md → pikku-software-archaeology/references/second-opinion-report-template.md} +0 -0
- /package/skills/{pikku-cli/references/complete-example.md → pikku-wiring/references/cli-complete-example.md} +0 -0
- /package/skills/{pikku-http → pikku-wiring}/references/http-options.md +0 -0
- /package/skills/{pikku-realtime/references/other-routes.md → pikku-wiring/references/realtime-other-routes.md} +0 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Running a persona as a virtual user
|
|
2
|
+
|
|
3
|
+
`pikku persona run <environment> <persona>` signs a declared persona in over the
|
|
4
|
+
app's real auth and works the API in character, driven by a model. A persona
|
|
5
|
+
while running **is** the virtual user — there is no second declaration for it.
|
|
6
|
+
|
|
7
|
+
**It is not a test runner.** It asserts nothing, and a green run proves nothing:
|
|
8
|
+
what it produces is _findings_, and their absence is only ever "not this time,
|
|
9
|
+
not with this seed". Findings set exit code 1, so a run can gate a pipeline;
|
|
10
|
+
giving up on a goal does not, because that is a user being a user.
|
|
11
|
+
|
|
12
|
+
Everything it needs is already in the project — the catalogue is the function
|
|
13
|
+
meta, the intents are the scenarios' own prose, the identity is the persona
|
|
14
|
+
signing in, the scopes come from their declared roles. The only new input is
|
|
15
|
+
which person to be.
|
|
16
|
+
|
|
17
|
+
Declaring personas — persona versus actor, `definePersonas`, materialised
|
|
18
|
+
actors — is in the skill itself, under **Personas and actors**. This is the
|
|
19
|
+
running half.
|
|
20
|
+
|
|
21
|
+
## The shape of a run
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
SCENARIO_ACTOR_SECRET=… pikku persona run local shopper
|
|
25
|
+
SCENARIO_ACTOR_SECRET=… pikku persona run local shopper -d careless --seed 42
|
|
26
|
+
SCENARIO_ACTOR_SECRET=… pikku persona run staging auditor \
|
|
27
|
+
--goals "reconcile the order totals" --steps 80 --out runs/auditor.json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Both arguments are required positionals: the environment key from
|
|
31
|
+
`environments`, then the persona id. A run needs a model — `--model`, or
|
|
32
|
+
`scenarios.model` in `pikku.config.json` — and an AI provider in the
|
|
33
|
+
environment (`OPENAI_BASE_URL` + `OPENAI_API_KEY`, or `LITELLM_PROXY_URL` +
|
|
34
|
+
`LITELLM_API_KEY`).
|
|
35
|
+
|
|
36
|
+
| Flag | Effect |
|
|
37
|
+
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
38
|
+
| `--disposition` / `-d` | How they behave. Overrides the persona's own |
|
|
39
|
+
| `--goals` | Comma-separated, in your words — run _alongside_ the persona's own and the ones derived from scenarios |
|
|
40
|
+
| `--steps` | Model turns before it stops (default 40) |
|
|
41
|
+
| `--mutations` | Non-read calls before it stops |
|
|
42
|
+
| `--duration` | Wall clock before it stops, e.g. `30m` |
|
|
43
|
+
| `--seed` | Replay — the same seed schedules the same run |
|
|
44
|
+
| `--model` | The model they think with |
|
|
45
|
+
| `--allow-approval` | Offer the endpoints the app marked as needing a human's approval. Off by default: those are the ones that spend money |
|
|
46
|
+
| `--skip-role-check` | Start without verifying declared roles against the stage |
|
|
47
|
+
| `--api-url` | Override the environment's `apiUrl`, for a target that only exists at run time. It replaces the url, not the environment's classification — see below |
|
|
48
|
+
| `--out` | Write the whole run — every step, response and finding — as JSON |
|
|
49
|
+
|
|
50
|
+
## The dispositions
|
|
51
|
+
|
|
52
|
+
A disposition is a bundle of instructions and mechanical dials (move weights,
|
|
53
|
+
temperature, repeat and re-read rates). `tuning` on the persona adjusts those
|
|
54
|
+
dials without replacing the character — a tuned `careless` user is still
|
|
55
|
+
careless. Passing `--disposition` drops the persona's `tuning`, because you
|
|
56
|
+
asked to run them differently rather than to bend their dials into another
|
|
57
|
+
shape.
|
|
58
|
+
|
|
59
|
+
| Disposition | Who that is |
|
|
60
|
+
| ------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
61
|
+
| `realistic` | The default. A competent user reading schemas and entering plausible values |
|
|
62
|
+
| `careless` | Busy, interrupted, half-remembering; submits twice, enters odd-but-legal values. **Where most production bugs actually live** |
|
|
63
|
+
| `newcomer` | First time here, holds no ids in their head, must find a path from the lists that exist (`emptyMemory`) |
|
|
64
|
+
| `stale` | Working from old notes — reaches for ids that may no longer resolve, to see how the product says so |
|
|
65
|
+
| `auditor` | Reconciling, not achieving: reads one fact from every endpoint claiming to know it and reports disagreement. Read-only |
|
|
66
|
+
| `adversarial` | Probing whether the boundaries are enforced. Inverted oracle — a 2xx from something it should not reach is the finding |
|
|
67
|
+
| `accountable` | Doing the job for real. The **only** disposition production accepts |
|
|
68
|
+
|
|
69
|
+
## Credentials, and which one wins
|
|
70
|
+
|
|
71
|
+
Three variables, checked in this order. None of them belongs in
|
|
72
|
+
`pikku.config.json`.
|
|
73
|
+
|
|
74
|
+
1. **`FABRIC_OPERATOR_TOKEN`** — what a deployed stage accepts. Asymmetric, and
|
|
75
|
+
it needs no account the target would not otherwise have, so it wins over the
|
|
76
|
+
other two when both are present.
|
|
77
|
+
2. **`PIKKU_PERSONA_SECRETS`** — `id=secret,id=secret`, already-derived
|
|
78
|
+
per-persona credentials. Hand a run only the personas it should be able to
|
|
79
|
+
be; asking for one outside the list is refused by name rather than falling
|
|
80
|
+
through to the root. Mint them with `pikku persona secret [personas...]` —
|
|
81
|
+
naming none mints all.
|
|
82
|
+
3. **`SCENARIO_ACTOR_SECRET`** — the root secret, which derives every persona's
|
|
83
|
+
credential and is therefore entitled to all of them. Only `pikku dev` serves
|
|
84
|
+
the endpoint it opens.
|
|
85
|
+
|
|
86
|
+
## Production is opt-in, twice
|
|
87
|
+
|
|
88
|
+
A persona's `environments` omitted means every configured environment **except**
|
|
89
|
+
those flagged `production: true` — nothing reaches production by being
|
|
90
|
+
forgotten. Naming one requires `disposition: 'accountable'`.
|
|
91
|
+
|
|
92
|
+
That rule is checked twice on purpose: the inspector checks the declaration at
|
|
93
|
+
build time, and sign-in re-checks the **effective** disposition — the persona's
|
|
94
|
+
own, or whatever `--disposition` replaced it with — before the run starts. So
|
|
95
|
+
`--disposition adversarial` cannot point an accountable persona at production.
|
|
96
|
+
The build check trusts the file; the run check does not trust which artifact got
|
|
97
|
+
deployed.
|
|
98
|
+
|
|
99
|
+
**That rule is keyed on the environment's name, not its url.** `production:
|
|
100
|
+
true` is a label a person wrote in `pikku.config.json`; nothing can tell from a
|
|
101
|
+
url whether real customers are behind it. `--api-url` replaces the url and keeps
|
|
102
|
+
the classification, so a non-production environment repointed at a production
|
|
103
|
+
host is still treated as non-production, and an adversarial persona will happily
|
|
104
|
+
run against it. The flag is for a target that only exists at run time — a
|
|
105
|
+
freshly provisioned sandbox. Point it anywhere else and the guard above is not
|
|
106
|
+
protecting you.
|
|
107
|
+
|
|
108
|
+
## The role check happens before the first step
|
|
109
|
+
|
|
110
|
+
A run reads its own roles back from the stage and compares them to what the
|
|
111
|
+
persona declared. It refuses on a mismatch, before anything runs — findings
|
|
112
|
+
from a persona whose roles drifted are about the seed, and reading them as
|
|
113
|
+
product bugs is how a whole run gets thrown away. A stage that reports no roles
|
|
114
|
+
warns and runs unverified. `--skip-role-check` is for a target whose auth
|
|
115
|
+
reports roles somewhere pikku cannot read; findings from such a run may be seed
|
|
116
|
+
drift.
|
|
117
|
+
|
|
118
|
+
## The other subcommands
|
|
119
|
+
|
|
120
|
+
| Command | What it answers |
|
|
121
|
+
| ------------------------------------ | ------------------------------------------------------------------------------------------ |
|
|
122
|
+
| `pikku persona list` | Who is declared — who each one is, what they may do, what they want |
|
|
123
|
+
| `pikku persona sync <environment>` | Which personas that environment will provision, with which roles, and why any were skipped |
|
|
124
|
+
| `pikku persona secret [personas...]` | Mint per-persona credentials from the root secret |
|
|
125
|
+
|
|
126
|
+
`sync` **reports**; it does not provision. The CLI has no connection to a
|
|
127
|
+
deployed environment's database, so the provisioning happens in the deployment —
|
|
128
|
+
pass the generated personas to `pikkuFabric` from `@pikku/better-auth`.
|
|
129
|
+
|
|
130
|
+
## What NOT to do
|
|
131
|
+
|
|
132
|
+
- **Do not treat a clean run as a pass.** Nothing was asserted. Use scenarios
|
|
133
|
+
for the things that must hold.
|
|
134
|
+
- **Do not run a persona declared `runnable: false`**, or one whose `account`
|
|
135
|
+
names a provider. The first is someone who exists to be acted upon — running
|
|
136
|
+
her races the scenario that bans her — and the second needs a human at a
|
|
137
|
+
consent screen. Both are refused before sign-in rather than partway through.
|
|
138
|
+
- **Do not use `--api-url` to reach a production host from a non-production
|
|
139
|
+
environment.** The disposition guard reads the named environment's
|
|
140
|
+
`production` flag, not the url you pointed it at, so nothing will stop you.
|
|
141
|
+
- **Do not put any of the three credentials in `pikku.config.json`.** They are
|
|
142
|
+
environment variables.
|
|
143
|
+
- **Do not pass `--allow-approval` casually.** The endpoints behind it are the
|
|
144
|
+
ones the app marked as needing a human because they spend money.
|
|
145
|
+
- **Do not read a finding from a run started with `--skip-role-check` as a
|
|
146
|
+
product bug** until the roles are confirmed some other way.
|
|
147
|
+
- **Do not expect `--goals` to replace the persona's goals.** They are appended;
|
|
148
|
+
a run that replaces Susan's goals is not Susan.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pikku-service-backends
|
|
3
|
+
description: >-
|
|
4
|
+
Use when picking or wiring a backend for one of Pikku's core service interfaces — ContentService
|
|
5
|
+
(S3, Backblaze B2), QueueService (SQS), SecretService (AWS Secrets Manager, Redis, MongoDB),
|
|
6
|
+
SchemaService (AJV, cfworker), ChannelStore, EventHubStore, WorkflowService, SessionStore or
|
|
7
|
+
AgentRunService (Redis, MongoDB). Covers which backend to choose, what each one silently does
|
|
8
|
+
differently, and the failures they swallow. TRIGGER when: code uses S3Content, B2Content,
|
|
9
|
+
SQSQueueService, AWSSecrets, RedisChannelStore, RedisSecretService, MongoDBChannelStore,
|
|
10
|
+
PikkuMongoDB, AjvSchemaService or CFWorkerSchemaService, or the user asks how to store files,
|
|
11
|
+
secrets, channel state or sessions. DO NOT TRIGGER when: defining service factories themselves
|
|
12
|
+
(use pikku-services), SQL via Kysely (use pikku-kysely), or the Lambda/Cloudflare runtimes
|
|
13
|
+
themselves (use pikku-deploy).
|
|
14
|
+
installGroups: [core]
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Pikku Service Backends
|
|
18
|
+
|
|
19
|
+
## Agent Operating Procedure
|
|
20
|
+
|
|
21
|
+
Use this skill as an execution checklist, not reference material.
|
|
22
|
+
|
|
23
|
+
1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.
|
|
24
|
+
2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.
|
|
25
|
+
3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.
|
|
26
|
+
4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.
|
|
27
|
+
5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.
|
|
28
|
+
|
|
29
|
+
Constructor shapes and method signatures come from `pikku doc` — run
|
|
30
|
+
`pikku doc --ai` for the installed surface. This skill is the part the compiler
|
|
31
|
+
cannot tell you: which backend implements which interface, and what changes when
|
|
32
|
+
you swap one for another.
|
|
33
|
+
|
|
34
|
+
`pikku-services` covers how to build and wire a service. This covers what to put
|
|
35
|
+
behind the interface.
|
|
36
|
+
|
|
37
|
+
## Pick a backend
|
|
38
|
+
|
|
39
|
+
| Interface | Backends | Package |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| `ContentService` | `S3Content`, `B2Content` | `@pikku/aws-services`, `@pikku/backblaze` |
|
|
42
|
+
| `QueueService` | `SQSQueueService` | `@pikku/aws-services` |
|
|
43
|
+
| `SecretService` | `AWSSecrets`, `RedisSecretService`, `MongoDBSecretService` | `@pikku/aws-services`, `@pikku/redis`, `@pikku/mongodb` |
|
|
44
|
+
| `SchemaService` | `AjvSchemaService`, `CFWorkerSchemaService` | `@pikku/schema-ajv`, `@pikku/schema-cfworker` |
|
|
45
|
+
| `ChannelStore`, `EventHubStore` | Redis, MongoDB | `@pikku/redis`, `@pikku/mongodb` |
|
|
46
|
+
| `PikkuWorkflowService`, `WorkflowRunService` | Redis, MongoDB | `@pikku/redis`, `@pikku/mongodb` |
|
|
47
|
+
| `SessionStore`, `AgentRunService`, `DeploymentService` | Redis, MongoDB | `@pikku/redis`, `@pikku/mongodb` |
|
|
48
|
+
| `AgentStorageService`, `AgentRunStateService` | MongoDB **only** | `@pikku/mongodb` |
|
|
49
|
+
|
|
50
|
+
SQL is the third option for every store interface in that table —
|
|
51
|
+
`KyselyChannelStore`, `KyselyWorkflowService`, `KyselySecretService` and friends
|
|
52
|
+
live in `@pikku/kysely` and are covered by `pikku-kysely`, because using them
|
|
53
|
+
means writing queries.
|
|
54
|
+
|
|
55
|
+
Per-package detail: `references/aws.md`, `references/backblaze.md`,
|
|
56
|
+
`references/redis.md`, `references/mongodb.md`, `references/schema.md`.
|
|
57
|
+
|
|
58
|
+
## What changes when you swap a backend
|
|
59
|
+
|
|
60
|
+
### Redis and MongoDB are not interchangeable, in two ways
|
|
61
|
+
|
|
62
|
+
They cover almost the same interface list, but:
|
|
63
|
+
|
|
64
|
+
- **MongoDB has AI conversation storage and Redis does not.**
|
|
65
|
+
`MongoDBAgentStorageService` is the only implementation of
|
|
66
|
+
`AgentStorageService`/`AgentRunStateService`. A Redis-only deployment cannot
|
|
67
|
+
persist agent conversations.
|
|
68
|
+
- **Every MongoDB service needs `await init()`; no Redis service does.** `init()`
|
|
69
|
+
is what creates the collections and indexes. Constructing a
|
|
70
|
+
`MongoDBChannelStore` and using it without awaiting `init()` compiles and then
|
|
71
|
+
behaves like an unindexed collection — slow first, wrong later.
|
|
72
|
+
|
|
73
|
+
Redis services take the connection directly (an ioredis `Redis`, `RedisOptions`,
|
|
74
|
+
or a URL string). MongoDB services take a `Db`, which means a `PikkuMongoDB`
|
|
75
|
+
wrapper has to be constructed and initialised before any of them.
|
|
76
|
+
|
|
77
|
+
### The two content backends share a design and a trap
|
|
78
|
+
|
|
79
|
+
`S3Content` and `B2Content` are close enough to swap, and both:
|
|
80
|
+
|
|
81
|
+
- treat `bucket` on every call as a **logical** bucket stored as a path prefix
|
|
82
|
+
(`${bucket}/${key}`) inside the one real bucket the config names. Do not
|
|
83
|
+
provision a bucket per logical bucket — the config takes exactly one.
|
|
84
|
+
- **ignore `visibility` on `getUploadURL`**.
|
|
85
|
+
- **swallow write failures**: `writeFile`, `copyFile` and `deleteFile` log and
|
|
86
|
+
return `false` rather than throwing, while the read paths throw. An ignored
|
|
87
|
+
return value is a silently lost file.
|
|
88
|
+
|
|
89
|
+
Where they diverge:
|
|
90
|
+
|
|
91
|
+
| | `S3Content` | `B2Content` |
|
|
92
|
+
| --- | --- | --- |
|
|
93
|
+
| Signing failure | **Fails open** — logs and returns the *unsigned* URL | Throws |
|
|
94
|
+
| `writeFile` memory | Streams | **Buffers the whole stream** to compute a SHA-1 |
|
|
95
|
+
| Client-side upload integrity | Presigned, expires at a fixed 3600s | `X-Bz-Content-Sha1: do_not_verify` — unverified |
|
|
96
|
+
| Credential rotation | Picked up by the SDK provider chain | Auth is cached for the instance's lifetime — construct a new `B2Content` |
|
|
97
|
+
|
|
98
|
+
The S3 fail-open is the one to design around: on a private CloudFront
|
|
99
|
+
distribution the client gets a 403, and on a public one you have just handed out
|
|
100
|
+
an unrestricted link. Validate `signConfig` at boot rather than trusting a throw.
|
|
101
|
+
|
|
102
|
+
### Secret backends differ on whether the app can write
|
|
103
|
+
|
|
104
|
+
- **`AWSSecrets` is read-only.** `setSecret` and `deleteSecret` throw. Secrets
|
|
105
|
+
are managed out of band; the app only reads them.
|
|
106
|
+
- **Redis and MongoDB do envelope encryption** and can write, delete, and
|
|
107
|
+
`rotateKEK()`. Rotation requires `previousKey` to have been set — a service
|
|
108
|
+
constructed without it cannot rotate later without a redeploy.
|
|
109
|
+
- **Only MongoDB has audit hooks** (`audit`, `auditReads`).
|
|
110
|
+
|
|
111
|
+
`AWSSecrets` also collapses every failure — missing, denied, binary-only — into
|
|
112
|
+
the same `FATAL: Error finding secret: <id>`, with the real reason on the error's
|
|
113
|
+
`cause`. Read `cause` before concluding a secret is absent; `hasSecret` returns
|
|
114
|
+
`false` for any error and cannot distinguish the two either.
|
|
115
|
+
|
|
116
|
+
### AJV and cfworker are not drop-in equivalents
|
|
117
|
+
|
|
118
|
+
Swapping them changes behaviour without changing types:
|
|
119
|
+
|
|
120
|
+
- **`useDefaults`**: AJV fills schema defaults into the validated object in
|
|
121
|
+
place. cfworker does not, so a field you relied on being defaulted arrives
|
|
122
|
+
`undefined` on Workers.
|
|
123
|
+
- **Recompilation**: AJV caches by name for the process lifetime — a second
|
|
124
|
+
`compileSchema` with the same name is a no-op. cfworker replaces the validator
|
|
125
|
+
when the schema value changes, which is what lets a dev hot-reload pick up
|
|
126
|
+
regenerated schemas. On AJV, restart the process instead.
|
|
127
|
+
- **Coercion is neither one's job.** `coerceTypes` is off; a query-string `"1"`
|
|
128
|
+
becomes `1` in the wiring layer, not here.
|
|
129
|
+
|
|
130
|
+
Both throw `UnprocessableContentError` (422) on a failed validation, and both
|
|
131
|
+
throw a **bare string** — `Missing validator for <name>` — for a *missing*
|
|
132
|
+
schema. It is not an `Error`, so `catch (e) { e.message }` reads `undefined`.
|
|
133
|
+
That almost always means codegen did not run.
|
|
134
|
+
|
|
135
|
+
Use cfworker on Cloudflare Workers: AJV compiles with `new Function`, which the
|
|
136
|
+
Workers runtime forbids.
|
|
137
|
+
|
|
138
|
+
### SQS gives you no result back
|
|
139
|
+
|
|
140
|
+
`SQSQueueService` sets `supportsResults = false` and `getJob()` always throws —
|
|
141
|
+
the transport is fire-and-forget. So is the Azure Storage Queue backend. Reach
|
|
142
|
+
for BullMQ or PgBoss (see `pikku-wiring`) when a caller needs the job's result.
|
|
143
|
+
|
|
144
|
+
## What NOT to do
|
|
145
|
+
|
|
146
|
+
- Do not ignore the boolean from `writeFile`, `copyFile` or `deleteFile`. Both
|
|
147
|
+
content backends report failure that way and neither throws.
|
|
148
|
+
- Do not rely on `S3Content.signURL` throwing. It fails open and hands back an
|
|
149
|
+
unsigned URL.
|
|
150
|
+
- Do not construct a MongoDB-backed service without awaiting `init()`.
|
|
151
|
+
- Do not assume AJV and cfworker validate identically — `useDefaults` alone
|
|
152
|
+
changes what your function receives.
|
|
153
|
+
- Do not provision one real bucket per logical bucket; the prefix is the bucket.
|
|
154
|
+
- Do not reach for SQS when a caller needs the result of the job.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# AWS (`@pikku/aws-services`)
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
yarn add @pikku/aws-services
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
AWS-backed implementations of the content, queue, and secret interfaces.
|
|
8
|
+
|
|
9
|
+
## `S3Content` — ContentService
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { S3Content } from '@pikku/aws-services'
|
|
13
|
+
|
|
14
|
+
const content = new S3Content(
|
|
15
|
+
config: { bucketName: string; region: string; endpoint?: string },
|
|
16
|
+
logger: Logger,
|
|
17
|
+
signConfig: { keyPairId: string; privateKey: string }
|
|
18
|
+
)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`endpoint` is what points the client at LocalStack or an S3-compatible store.
|
|
22
|
+
|
|
23
|
+
Every method takes a single **args object**, matching the shared `ContentService`
|
|
24
|
+
interface. None of them are positional:
|
|
25
|
+
|
|
26
|
+
- `signURL({ url, dateLessThan, dateGreaterThan? }): Promise<string>` — CloudFront-sign an absolute URL
|
|
27
|
+
- `signContentKey({ bucket, contentKey, dateLessThan, dateGreaterThan? }): Promise<string>`
|
|
28
|
+
- `getUploadURL({ bucket, fileKey, contentType, visibility? }): Promise<{ uploadUrl, assetKey }>` — `visibility` is ignored
|
|
29
|
+
- `readFile({ bucket, key }): Promise<ReadableStream | NodeJS.ReadableStream>`
|
|
30
|
+
- `readFileAsBuffer({ bucket, key }): Promise<Buffer>`
|
|
31
|
+
- `writeFile({ bucket, key, stream }): Promise<boolean>`
|
|
32
|
+
- `copyFile({ bucket, key, fromAbsolutePath }): Promise<boolean>`
|
|
33
|
+
- `deleteFile({ bucket, key }): Promise<boolean>`
|
|
34
|
+
|
|
35
|
+
`signContentKey` builds `https://<bucketName>/<bucket>/<contentKey>` — it uses
|
|
36
|
+
`bucketName` as the **host**. For signed content that value must therefore be
|
|
37
|
+
your CloudFront domain, not a plain bucket name, which means the same config
|
|
38
|
+
field is doing two jobs.
|
|
39
|
+
|
|
40
|
+
Presigned upload URLs expire after a fixed **3600s**, not configurable through
|
|
41
|
+
the service.
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
const createSingletonServices = pikkuServices(async (config) => {
|
|
45
|
+
const logger = new PinoLogger()
|
|
46
|
+
const content = new S3Content(
|
|
47
|
+
{ bucketName: config.s3Bucket, region: config.awsRegion },
|
|
48
|
+
logger,
|
|
49
|
+
{ keyPairId: config.cfKeyPairId, privateKey: config.cfPrivateKey }
|
|
50
|
+
)
|
|
51
|
+
return { config, logger, content }
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## `SQSQueueService` — QueueService
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { SQSQueueService } from '@pikku/aws-services'
|
|
59
|
+
|
|
60
|
+
const queue = new SQSQueueService({
|
|
61
|
+
region: string,
|
|
62
|
+
queueUrlPrefix: string, // e.g. 'https://sqs.us-east-1.amazonaws.com/123456789/'
|
|
63
|
+
endpoint?: string, // LocalStack or a custom SQS endpoint
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- `add<T>(queueName: string, data: T, options?: JobOptions): Promise<string>` — returns SQS's `MessageId`
|
|
68
|
+
- `getJob()` — always **throws**
|
|
69
|
+
|
|
70
|
+
The queue URL is `queueUrlPrefix + queueName`, so the name in `wireQueueWorker`
|
|
71
|
+
has to match the SQS queue exactly.
|
|
72
|
+
|
|
73
|
+
Constraints inherited from SQS, enforced in `add`:
|
|
74
|
+
|
|
75
|
+
- `options.delay` is in **milliseconds**, floored to whole seconds. Over
|
|
76
|
+
900_000ms (15 minutes) or negative throws before the message is sent.
|
|
77
|
+
- Standard queues only — no FIFO, so no `MessageGroupId` and no ordering
|
|
78
|
+
guarantee.
|
|
79
|
+
- `data` is `JSON.stringify`d, which is where a `Date` or a `Map` quietly
|
|
80
|
+
degrades.
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
const createSingletonServices = pikkuServices(async (config) => {
|
|
84
|
+
const queue = new SQSQueueService({
|
|
85
|
+
region: config.awsRegion,
|
|
86
|
+
queueUrlPrefix: config.sqsUrlPrefix,
|
|
87
|
+
})
|
|
88
|
+
return { config, queue }
|
|
89
|
+
})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## `AWSSecrets` — SecretService
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { AWSSecrets } from '@pikku/aws-services'
|
|
96
|
+
|
|
97
|
+
const secrets = new AWSSecrets({ awsRegion: 'eu-west-2' })
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`AWSConfig` has one field, `awsRegion` — there is no credentials option; the
|
|
101
|
+
SDK's default provider chain (instance role, env, profile) supplies those.
|
|
102
|
+
|
|
103
|
+
- `getSecret<T = string>(SecretId: string): Promise<SecretValue<T>>` — a JSON secret is parsed automatically, so pass a shape as `T` (a non-JSON value comes back as the raw string). The result is a branded `SecretValue`, not a bare value — reveal it where it is used rather than passing it through logs
|
|
104
|
+
- `getSecrets<T>(SecretIds: (keyof T & string)[]): Promise<Partial<T>>` — missing keys are omitted rather than thrown
|
|
105
|
+
- `hasSecret(SecretId: string): Promise<boolean>` — performs a full fetch
|
|
106
|
+
- `setSecret` / `deleteSecret` — **not implemented**; they throw
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Backblaze B2 (`@pikku/backblaze`)
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
yarn add @pikku/backblaze
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
`B2Content` implements `ContentService` over Backblaze B2.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { B2Content } from '@pikku/backblaze'
|
|
11
|
+
|
|
12
|
+
const content = new B2Content(config: B2ContentConfig, logger: Logger)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`B2ContentConfig` has exactly three fields — `applicationKeyId`, `applicationKey`
|
|
16
|
+
and `bucketId`. There is no `cdnUrl`: downloads are served from the `downloadUrl`
|
|
17
|
+
B2 returns at authorization.
|
|
18
|
+
|
|
19
|
+
Every method takes a single **args object**, matching the shared `ContentService`
|
|
20
|
+
interface. None of them are positional:
|
|
21
|
+
|
|
22
|
+
- `signContentKey({ bucket, contentKey, dateLessThan }): Promise<string>` — a full download URL with an `Authorization` query param
|
|
23
|
+
- `signURL({ url, dateLessThan }): Promise<string>` — re-signs an existing `/file/` URL; a URL with no `/file/` segment is returned untouched
|
|
24
|
+
- `getUploadURL({ bucket, fileKey, contentType, visibility? }): Promise<UploadURLResult>` — `visibility` is ignored
|
|
25
|
+
- `writeFile({ bucket, key, stream }): Promise<boolean>`
|
|
26
|
+
- `copyFile({ bucket, key, fromAbsolutePath }): Promise<boolean>`
|
|
27
|
+
- `readFile({ bucket, key }): Promise<ReadableStream | NodeJS.ReadableStream>`
|
|
28
|
+
- `readFileAsBuffer({ bucket, key }): Promise<Buffer>`
|
|
29
|
+
- `deleteFile({ bucket, key }): Promise<boolean>`
|
|
30
|
+
|
|
31
|
+
Because `writeFile` drains the whole stream into a `Buffer` before uploading
|
|
32
|
+
(B2's upload endpoint needs a SHA-1 and a content length up front), large uploads
|
|
33
|
+
should go through `getUploadURL` and be sent by the client directly.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
const createSingletonServices = pikkuServices(async (config) => {
|
|
37
|
+
const logger = new PinoLogger()
|
|
38
|
+
const content = new B2Content(
|
|
39
|
+
{
|
|
40
|
+
applicationKeyId: config.b2KeyId,
|
|
41
|
+
applicationKey: config.b2AppKey,
|
|
42
|
+
bucketId: config.b2BucketId,
|
|
43
|
+
},
|
|
44
|
+
logger
|
|
45
|
+
)
|
|
46
|
+
return { config, logger, content }
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
await content.writeFile({ bucket: 'avatars', key: `${userId}.png`, stream })
|
|
52
|
+
const url = await content.signContentKey({
|
|
53
|
+
bucket: 'avatars',
|
|
54
|
+
contentKey: `${userId}.png`,
|
|
55
|
+
dateLessThan: new Date(Date.now() + 60_000),
|
|
56
|
+
})
|
|
57
|
+
```
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# MongoDB (`@pikku/mongodb`)
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
yarn add @pikku/mongodb
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
## `PikkuMongoDB` — connection wrapper
|
|
8
|
+
|
|
9
|
+
Every service below takes a `Db`, so the wrapper is constructed and initialised
|
|
10
|
+
first.
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { PikkuMongoDB } from '@pikku/mongodb'
|
|
14
|
+
|
|
15
|
+
const mongo = new PikkuMongoDB(
|
|
16
|
+
logger: Logger,
|
|
17
|
+
clientOrUri: MongoClient | string,
|
|
18
|
+
dbName: string,
|
|
19
|
+
options?: MongoClientOptions
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
await mongo.init()
|
|
23
|
+
mongo.db // Db instance for queries
|
|
24
|
+
await mongo.close()
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Available services
|
|
28
|
+
|
|
29
|
+
| Service | Interface | Purpose |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| `MongoDBChannelStore` | `ChannelStore` | WebSocket channel state persistence |
|
|
32
|
+
| `MongoDBEventHubStore` | `EventHubStore` | Event hub state persistence |
|
|
33
|
+
| `MongoDBWorkflowService` | `PikkuWorkflowService` | Workflow definition storage |
|
|
34
|
+
| `MongoDBWorkflowRunService` | `WorkflowRunService` | Workflow execution tracking |
|
|
35
|
+
| `MongoDBDeploymentService` | `DeploymentService` | Deployment state management |
|
|
36
|
+
| `MongoDBAgentStorageService` | `AgentStorageService`, `AgentRunStateService` | AI conversation/run storage |
|
|
37
|
+
| `MongoDBAgentRunService` | `AgentRunService` | Agent execution tracking |
|
|
38
|
+
| `MongoDBSecretService` | `SecretService` | Encrypted secret storage (envelope encryption) |
|
|
39
|
+
| `MongoDBSessionStore` | `SessionStore` | Persisted user sessions |
|
|
40
|
+
|
|
41
|
+
All of them take a `Db` in the constructor and have an `init()` method that
|
|
42
|
+
creates the collections and indexes. **Await it** — a service used without it
|
|
43
|
+
behaves like an unindexed collection.
|
|
44
|
+
|
|
45
|
+
## `MongoDBSecretService`
|
|
46
|
+
|
|
47
|
+
Envelope encryption: `key` derives the KEK that wraps each secret's own DEK.
|
|
48
|
+
Keeping `previousKey` set is what makes `rotateKEK()` possible — it re-wraps
|
|
49
|
+
every secret onto the current key and returns the new version.
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { MongoDBSecretService } from '@pikku/mongodb'
|
|
53
|
+
|
|
54
|
+
const secrets = new MongoDBSecretService(mongo.db, {
|
|
55
|
+
key: 'your-key-encryption-passphrase',
|
|
56
|
+
keyVersion: 2, // defaults to 1
|
|
57
|
+
previousKey: 'the-passphrase-you-are-rotating-away-from',
|
|
58
|
+
audit: true, // log write/delete/rotate through the audit sink
|
|
59
|
+
auditReads: false, // reads too — noisy, off by default
|
|
60
|
+
})
|
|
61
|
+
await secrets.init()
|
|
62
|
+
|
|
63
|
+
await secrets.setSecret('api-key', { key: 'sk-...' })
|
|
64
|
+
const value = await secrets.getSecret<{ key: string }>('api-key')
|
|
65
|
+
await secrets.rotateKEK()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Full setup
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import {
|
|
72
|
+
PikkuMongoDB,
|
|
73
|
+
MongoDBChannelStore,
|
|
74
|
+
MongoDBWorkflowService,
|
|
75
|
+
} from '@pikku/mongodb'
|
|
76
|
+
|
|
77
|
+
const createSingletonServices = pikkuServices(async (config) => {
|
|
78
|
+
const logger = new PinoLogger()
|
|
79
|
+
const mongo = new PikkuMongoDB(logger, config.mongoUri, 'myapp')
|
|
80
|
+
await mongo.init()
|
|
81
|
+
|
|
82
|
+
const channelStore = new MongoDBChannelStore(mongo.db)
|
|
83
|
+
await channelStore.init()
|
|
84
|
+
|
|
85
|
+
const workflowService = new MongoDBWorkflowService(mongo.db)
|
|
86
|
+
await workflowService.init()
|
|
87
|
+
|
|
88
|
+
return { config, logger, database: mongo, channelStore, workflowService }
|
|
89
|
+
})
|
|
90
|
+
```
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Redis (`@pikku/redis`)
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
yarn add @pikku/redis
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Redis-backed implementations of Pikku's core service interfaces, using
|
|
8
|
+
[ioredis](https://github.com/redis/ioredis). Every service accepts a Redis
|
|
9
|
+
connection — an ioredis `Redis` instance, `RedisOptions`, or a connection
|
|
10
|
+
string — in its constructor. None of them need an `init()` call.
|
|
11
|
+
|
|
12
|
+
| Service | Interface | Purpose |
|
|
13
|
+
| --- | --- | --- |
|
|
14
|
+
| `RedisChannelStore` | `ChannelStore` | WebSocket channel state persistence |
|
|
15
|
+
| `RedisEventHubStore` | `EventHubStore` | Event hub state persistence |
|
|
16
|
+
| `RedisWorkflowService` | `PikkuWorkflowService` | Workflow definition storage |
|
|
17
|
+
| `RedisWorkflowRunService` | `WorkflowRunService` | Workflow execution tracking |
|
|
18
|
+
| `RedisDeploymentService` | `DeploymentService` | Deployment state management |
|
|
19
|
+
| `RedisAgentRunService` | `AgentRunService` | Agent execution tracking |
|
|
20
|
+
| `RedisSecretService` | `SecretService` | Encrypted secret storage (envelope encryption) |
|
|
21
|
+
| `RedisSessionStore` | `SessionStore` | Persisted user sessions |
|
|
22
|
+
|
|
23
|
+
There is no Redis implementation of `AgentStorageService` — AI conversation
|
|
24
|
+
storage is MongoDB-only.
|
|
25
|
+
|
|
26
|
+
## `RedisSecretService`
|
|
27
|
+
|
|
28
|
+
Envelope encryption: `key` derives the KEK that wraps each secret's own DEK.
|
|
29
|
+
Keeping `previousKey` set is what makes `rotateKEK()` possible — it re-wraps
|
|
30
|
+
every secret onto the current key and returns the new version.
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { RedisSecretService } from '@pikku/redis'
|
|
34
|
+
|
|
35
|
+
const secrets = new RedisSecretService(
|
|
36
|
+
connectionOrConfig: Redis | RedisOptions | string,
|
|
37
|
+
config: {
|
|
38
|
+
key: string // the KEK passphrase
|
|
39
|
+
keyVersion?: number // defaults to 1
|
|
40
|
+
previousKey?: string // required to rotate
|
|
41
|
+
keyPrefix?: string // namespaces the redis keys
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
await secrets.getSecret<T = string>(key: string): Promise<T>
|
|
46
|
+
await secrets.getSecrets<T>(keys: (keyof T & string)[]): Promise<Partial<T>>
|
|
47
|
+
await secrets.hasSecret(key: string): Promise<boolean>
|
|
48
|
+
await secrets.setSecret(key: string, value: unknown): Promise<void>
|
|
49
|
+
await secrets.deleteSecret(key: string): Promise<void>
|
|
50
|
+
await secrets.rotateKEK(): Promise<number>
|
|
51
|
+
await secrets.close(): Promise<void>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Full setup
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import {
|
|
58
|
+
RedisChannelStore,
|
|
59
|
+
RedisWorkflowService,
|
|
60
|
+
RedisSecretService,
|
|
61
|
+
} from '@pikku/redis'
|
|
62
|
+
|
|
63
|
+
const createSingletonServices = pikkuServices(async (config) => {
|
|
64
|
+
const logger = new PinoLogger()
|
|
65
|
+
|
|
66
|
+
const channelStore = new RedisChannelStore(config.redisUrl)
|
|
67
|
+
const workflowService = new RedisWorkflowService(config.redisUrl)
|
|
68
|
+
|
|
69
|
+
const secrets = new RedisSecretService(config.redisUrl, {
|
|
70
|
+
key: config.kekPassphrase,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
return { config, logger, channelStore, workflowService, secrets }
|
|
74
|
+
})
|
|
75
|
+
```
|