@voeu/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Solus-QE
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,362 @@
1
+ # voeu
2
+
3
+ Keep integrations working when providers change — and prove it.
4
+
5
+ A **contract pack** is one provider's rules, written down claim by claim, encoded in a simulator,
6
+ and turned into scenarios your application has to survive. Shopify's expiring offline access
7
+ tokens are the first pack and the one this page is mostly about; Stripe idempotency is the
8
+ second — see [Contract packs](#contract-packs) for what each one covers and what it does not.
9
+
10
+ Every public Shopify app that calls the GraphQL Admin API must use expiring offline access tokens
11
+ [from 1 January 2027](https://shopify.dev/changelog/expiring-offline-access-tokens-required-for-all-public-apps-as-of-january-1-2027).
12
+ Custom and merchant-created apps are exempt. After that date, apps still presenting a non-expiring
13
+ token get authentication errors, which one developer on Shopify's own thread described as
14
+ "you've essentially uninstalled the app from every one of your customer's stores."
15
+
16
+ The mechanical part of that migration is documented and mostly not the problem. What the docs
17
+ cannot tell you is whether **your** app survives the new lifecycle — your session storage, your
18
+ background jobs, your worker topology, your failure handling. Those are runtime properties.
19
+
20
+ ```bash
21
+ npx @voeu/cli inspect --repo . # read the source. no credentials, no execution.
22
+ npx @voeu/cli verify --reference # see what verify does, with no setup
23
+ npx @voeu/cli init --repo . # scaffold the adapter verify drives
24
+ npx @voeu/cli verify --repo . # run YOUR application against a simulated Shopify
25
+ npx @voeu/cli fix --repo . # emit a patch for the deterministic findings
26
+ npx @voeu/cli watch --record # record today, then run it nightly
27
+ npx @voeu/cli contract # print every rule this encodes, and its source
28
+
29
+ npx @voeu/cli verify --provider stripe --reference # the second pack: idempotency
30
+ ```
31
+
32
+ ## Why not a mock server
33
+
34
+ Pointing a mock or a sandbox at your application gives you a substrate: somewhere to make calls
35
+ that will not touch production. You still have to know which calls to make, what should happen,
36
+ and whether what happened was right. That is the part voeu does, and it is three things a mock
37
+ does not have.
38
+
39
+ **A verdict, not a transcript.** Fourteen scenarios ask questions with a right answer — does a job
40
+ enqueued before expiry still work when the queue reaches it, can an older session write overwrite
41
+ newer credentials — and each returns `passed`, `failed`, `not_applicable` or `not_verified` with
42
+ the consequence spelled out. A mock replies to requests; it has no opinion about whether your
43
+ handling of the reply was correct.
44
+
45
+ **A contract you can audit.** Every rule the simulator encodes is written down as data: the
46
+ statement, the Shopify document it came from, the constant it became, and the checks that stop
47
+ being believable if it moves. Run `voeu contract` and argue with it in five minutes instead of
48
+ reading a state machine. A mock's behaviour is whatever somebody typed into it, with no record of
49
+ why.
50
+
51
+ **An alarm when the rule moves.** Shopify changed the refresh recovery window from 60 minutes to
52
+ 30 days on 28 August 2026 — no migration required, nothing to do, and no reason for anybody to
53
+ notice that results verified the week before were verified against a rule that no longer existed.
54
+ `voeu watch` re-reads every cited document nightly and downgrades the checks that rested on a
55
+ phrase which has disappeared. A mock configured against the old rule keeps passing, confidently,
56
+ forever.
57
+
58
+ The first is why a run is worth reading. The third is why it is worth keeping.
59
+
60
+ ## `inspect`
61
+
62
+ Static. Reads source only — never executes your code, never calls a model, never needs a
63
+ credential. Runs in seconds on a repository it has never seen.
64
+
65
+ ```
66
+ voeu inspect — acme/inventory-sync
67
+
68
+ stack @shopify/shopify-app-remix ^5.0.1 · prisma sessions · distribution unknown
69
+ contract shopify-token-lifecycle@0.1.0
70
+
71
+ FAILED Session model is missing refreshToken and refreshTokenExpires
72
+ prisma/schema.prisma:10
73
+ FAILED Credential enqueued into deferred work via syncQueue.add
74
+ app/jobs/sync.server.ts:12:41
75
+ UNPROVEN No recognised per-shop serialisation around token refresh
76
+ Procfile:1
77
+
78
+ 4 failed · 2 unproven
79
+
80
+ Static analysis only. Nothing in this repository was executed.
81
+ Run `voeu verify` to decide the unproven checks.
82
+ ```
83
+
84
+ Because it reads source only, `inspect` is safe to run on a fork-originated pull request.
85
+
86
+ ## `verify`
87
+
88
+ Executes your application against a simulator that encodes Shopify's token lifecycle — 60-minute
89
+ access tokens, 90-day single-use refresh tokens, the
90
+ [2026-08-28 recovery window](https://shopify.dev/changelog/more-resilient-refreshes-for-expiring-offline-access-tokens),
91
+ and the failure modes you cannot provoke on your own: 5xx, rate limits, timeouts, and **lost
92
+ responses**, where the provider commits and your code never learns what happened.
93
+
94
+ Fourteen scenarios ask questions source cannot answer:
95
+
96
+ | Family | What the Shopify pack asks |
97
+ |---|---|
98
+ | `credential-lifecycle` | Does the pair survive expiry, restart, a transient 5xx, and a lost refresh response? |
99
+ | `deferred-execution` | Does a job enqueued before expiry still work when the queue reaches it? |
100
+ | `concurrency` | Do two processes refreshing one shop converge on one live pair — and can an older session write overwrite newer credentials? |
101
+ | `partial-failure` | Does a failed session write strand the shop? Is an ambiguous conversion replayed? |
102
+ | `tenant-isolation` | Can one merchant's credential serve another? |
103
+
104
+ Those five are the families that apply to Shopify's token lifecycle. The taxonomy has a sixth —
105
+ `retry-identity` — which does not, and is reported as not applicable rather than silently absent.
106
+ It is the main one the Stripe pack asks about, where the equivalent failure is a retry that mints
107
+ a fresh idempotency key and bills the customer twice.
108
+
109
+ `verify` needs one thing from you: an adapter. `voeu init` writes a starter one, already wired to
110
+ whatever `inspect` could find:
111
+
112
+ ```
113
+ $ npx @voeu/cli init --repo .
114
+ wrote voeu.config.json points verify at the adapter below
115
+ wrote voeu/adapter.ts wired to your Prisma session storage; queue methods left as TODO
116
+
117
+ Before `voeu verify` will run:
118
+ - enqueueJob() and runJobs() must call your real queue. Left as TODO because a
119
+ guess here would make the deferred-execution scenarios test the guess.
120
+ ```
121
+
122
+ The adapter is yours from the moment it exists — checked in, never regenerated, never overwritten.
123
+ That seam is deliberate: a harness that supplies its own session store proves only that the store
124
+ it supplied works. A partly-filled adapter still gives honest answers, because scenarios it cannot
125
+ exercise report UNPROVEN rather than passing.
126
+
127
+ `npx @voeu/cli verify --reference` runs the whole suite against a built-in application, so you can see
128
+ the output before writing anything.
129
+
130
+ ## `watch`
131
+
132
+ A verified result decays, for three reasons that have nothing to do with each other.
133
+ Your code changes. **The provider changes.** Time passes. Only the first is visible in
134
+ your repository, and the second is the one that took Shopify's own developers by
135
+ surprise: the refresh recovery window changed on
136
+ [28 August 2026](https://shopify.dev/changelog/more-resilient-refreshes-for-expiring-offline-access-tokens)
137
+ with no migration required and no reason for anybody to notice.
138
+
139
+ ```bash
140
+ npx @voeu/cli watch --record --repo . # once. commit voeu/baseline.json
141
+ npx @voeu/cli watch --repo . # nightly, in CI
142
+ ```
143
+
144
+ `watch` re-inspects your tree, re-reads every provider document this contract cites,
145
+ and reports only what moved.
146
+
147
+ ```
148
+ voeu watch — acme/inventory-sync
149
+
150
+ Something moved: 4 check(s) went stale.
151
+
152
+ contract shopify-token-lifecycle@0.1.0
153
+ deadline 114 days to 1 January 2027
154
+
155
+ SAME shopify.dev/docs/.../migrate-to-expiring-offline-access-tokens
156
+ DRIFTED shopify.dev/changelog/more-resilient-refreshes-...
157
+ Text this contract depends on is no longer in the document:
158
+ "limited to 30 days from the original refresh token".
159
+ ```
160
+
161
+ Two strengths of alarm, and only one of them is loud. **EDITED** means the page changed
162
+ but every phrase the contract depends on is still there — worth a read, not evidence
163
+ that a rule moved. **DRIFTED** means a phrase this contract rests on is gone, and every
164
+ check resting on it is downgraded from passed to unverified until a human looks.
165
+
166
+ Three rules make this usable rather than noisy:
167
+
168
+ - **A phrase is only armed if it was seen to match when you recorded.** A guessed phrase
169
+ that never matched cannot tell drift from a bad guess, so it is reported as an unwatched
170
+ claim instead of firing on day one. When this file was first written it claimed the
171
+ migration guide said "60 minutes" and "90 days". It says neither — both numbers live in
172
+ the changelogs — and `--record` caught it in under a minute.
173
+ - **A document that could not be read is never reported as unchanged.** It gets its own
174
+ state, because "we could not check" and "nothing changed" are different sentences.
175
+ - **Nothing re-records itself.** A watch that acknowledged the drift it found would be a
176
+ green light wired to its own alarm.
177
+
178
+ A finding that was already in your baseline does not fail the job. `watch` answers "has
179
+ anything changed?", not "is this broken?" — a nightly job that is red every night is a
180
+ nightly job that gets switched off.
181
+
182
+ ## `contract`
183
+
184
+ The simulator is only worth trusting if you can check it, so every rule it encodes is
185
+ written down as data: the statement, the Shopify document it came from, the constant it
186
+ became, and the checks that stop being believable if it moves.
187
+
188
+ ```bash
189
+ npx @voeu/cli contract # all of it
190
+ npx @voeu/cli contract --open # only where the documentation is ambiguous
191
+ ```
192
+
193
+ The `--open` list is the part nobody volunteers. Three of the claims rest on a reading
194
+ rather than a rule — including the status code Shopify returns for a rejected refresh,
195
+ which no page states and which applications branch on. Each one names the request
196
+ sequence that would settle it against a real store.
197
+
198
+ ## `differential`
199
+
200
+ The one command that can catch **Voeu** being wrong.
201
+
202
+ ```bash
203
+ npx @voeu/cli differential --self-check # no credentials. proves the comparison works.
204
+ npx @voeu/cli differential --live # a development store you own. proves the contract.
205
+ ```
206
+
207
+ Every `verify` result rests on the simulator being a faithful account of Shopify, and
208
+ until it has been compared against a real store that is an assertion rather than
209
+ evidence. `--live` runs the contract's open questions against a development store and
210
+ reports where the two disagree — as a finding **about the simulator**, never about your
211
+ application.
212
+
213
+ `--self-check` needs nothing. It runs the probes against the simulator twice, which must
214
+ agree, and then against mutations of it that answer an open question the other way, which
215
+ must be detected. That establishes the comparison works. It establishes nothing about
216
+ Shopify, and it says so on every run.
217
+
218
+ ## `fix`
219
+
220
+ Emits a patch for the findings that are deterministic — the two columns Shopify documents, the
221
+ flag Shopify documents, and the additive migration to go with them. No model is involved, so the
222
+ patch is reproducible from your commit.
223
+
224
+ It never edits your working tree. It writes a patch, **re-runs the same checks over the patched
225
+ tree**, and reports only what the re-check confirms. Findings it cannot repair safely — a
226
+ credentialed client at module scope, a credential in a queue payload — are declined explicitly
227
+ with reasons, because a repair tool that quietly skips the hard finding leaves you believing it
228
+ was handled.
229
+
230
+ ## GitHub Action
231
+
232
+ ```yaml
233
+ - uses: Solus-QE/voeu@v0
234
+ with:
235
+ command: inspect
236
+ distribution: public # optional; see below
237
+ ```
238
+
239
+ Writes `run.json` and a self-contained `report.html`, adds a job summary, and uploads both as an
240
+ artifact.
241
+
242
+ The Action lives in a repository that is not yet public, so `uses: Solus-QE/voeu@v0` resolves only
243
+ for workflows in that account until it is. Everything the Action does, the CLI does directly:
244
+
245
+ ```yaml
246
+ - run: npx @voeu/cli@0.1 inspect --repo . --out voeu-run
247
+ ```
248
+
249
+ exits `1` on a failing check, `2` when the target could not be assessed, and leaves
250
+ `voeu-run/inspect.json` and `voeu-run/report.html` to upload with `actions/upload-artifact`.
251
+
252
+ ### Reporting a run to Voeu Cloud
253
+
254
+ The hosted product reads your repository's archive and never executes it, so every verdict it
255
+ shows is static. `verify` is the runtime answer, and it runs in your CI. To put its result beside
256
+ the hosted inspect on the repository's page:
257
+
258
+ ```yaml
259
+ jobs:
260
+ verify:
261
+ if: github.event_name == 'push'
262
+ runs-on: ubuntu-latest
263
+ permissions:
264
+ contents: read
265
+ id-token: write # the only credential: GitHub's id-token for this run
266
+ steps:
267
+ - uses: actions/checkout@v4
268
+ - uses: Solus-QE/voeu@v0
269
+ with:
270
+ command: verify
271
+ upload: "true"
272
+ ```
273
+
274
+ The Action asks GitHub for an OIDC id-token with the `voeu` audience and POSTs the run with it.
275
+ Voeu verifies the token's signature, issuer and audience and takes the repository from its
276
+ `repository_id` claim — nothing in the run itself decides where it lands. There is no secret to
277
+ configure, and nothing to rotate. Fork-triggered workflows are never issued an id-token, which is
278
+ why this runs on `push` and not `pull_request`. Reporting never fails the job: a missing
279
+ permission, a repository without the Voeu GitHub App, or an unreachable endpoint is a warning in
280
+ the log, and the run's own exit code is unaffected.
281
+
282
+ ## Exit codes
283
+
284
+ | | |
285
+ |---|---|
286
+ | `0` | no failing check. For `watch`: the baseline still holds |
287
+ | `1` | at least one check failed. For `watch`: the baseline no longer holds |
288
+ | `2` | unsupported target or missing configuration |
289
+ | `3` | infrastructure failure |
290
+
291
+ `2` is deliberately not `1`. "We could not assess this" is not "your app is broken," and CI that
292
+ conflates them teaches people to ignore the tool. The Action leaves the job green on `2` with a
293
+ notice.
294
+
295
+ ## Four verdicts, never three
296
+
297
+ `passed` · `failed` · `not_applicable` · **`not_verified`**
298
+
299
+ `not_verified` is the load-bearing one. It means the check is in scope, applies to your
300
+ repository, and was not decided — the budget ran out, the stack was unsupported, an adapter
301
+ capability was missing. Folding it into `passed` is how a verification tool starts lying; folding
302
+ it into `failed` is how it starts crying wolf. It gets its own state and it is printed.
303
+
304
+ Every finding also carries **how** it was reached — `static`, `simulated`, or `live` — because
305
+ "this patch is verified" without an evidence level attached is the sentence this tool exists to
306
+ stop people from saying.
307
+
308
+ ## What it will not tell you
309
+
310
+ - **Whether your app is publicly distributed.** `shopify.app.toml` has no `distribution` field;
311
+ the setting lives in the Dev Dashboard and is not recoverable from source. Pass
312
+ `--distribution public` if you know, or treat findings as conditional.
313
+ - **That your migration is complete.** `verify` proves fourteen behaviours held against a
314
+ simulator. It does not cover webhook verification, online sessions, billing, or Admin API
315
+ operations beyond the one used to prove authentication.
316
+ - **That the simulator is right.** Where it diverges from Shopify, results are wrong with it.
317
+ Every report says so, and `voeu differential --live` is the command that closes it —
318
+ built, self-tested, and waiting on a Partner account rather than on more code. Until
319
+ that has run, no result in this repository carries the `live` evidence level.
320
+
321
+ Voeu also never runs token conversion against your installed merchants. Conversion is
322
+ [one-time and irreversible per shop](https://github.com/Shopify/shopify-app-js/blob/main/packages/apps/shopify-api/docs/reference/auth/migrateToExpiringToken.md);
323
+ that call stays yours to make.
324
+
325
+ ## Contract packs
326
+
327
+ Two, and they ask different questions — because the failure a provider can hand you is a
328
+ property of that provider, not a house style.
329
+
330
+ | Pack | Asks | Families exercised |
331
+ |---|---|---|
332
+ | `shopify-token-lifecycle@0.1.0` | Does your app survive a credential that expires, rotates, and can be lost mid-refresh? | credential-lifecycle, deferred-execution, concurrency, partial-failure, tenant-isolation |
333
+ | `stripe-idempotency@0.1.0` | Across a retry, does an operation keep its identity — and does a genuinely new one get a fresh one? | retry-identity, partial-failure, tenant-isolation |
334
+
335
+ Wrong in one direction bills the customer twice; wrong in the other silently no-ops. Neither is
336
+ visible in the source, which is why the Stripe pack exists at all.
337
+
338
+ ```bash
339
+ npx @voeu/cli verify --provider stripe --reference
340
+ ```
341
+
342
+ **What the Stripe pack does not do yet, stated plainly.** It runs against the built-in reference
343
+ application only. Verifying *your* Stripe integration needs an adapter interface this CLI does not
344
+ yet document, a scaffold for `voeu init`, and static detectors so `inspect` has something to say.
345
+ None of those are written, so `--provider stripe --repo .` exits 2 and says so rather than
346
+ reporting every scenario `not_verified` while looking supported.
347
+
348
+ `voeu contract --provider stripe` does work: it prints seven claims quoted from Stripe's
349
+ idempotency reference, two of them carrying an open question that a live account would settle.
350
+
351
+ `credential-lifecycle` and `deferred-execution` are reported **not applicable** for Stripe rather
352
+ than omitted: secret keys do not expire or rotate on a schedule, and an absence that looks like
353
+ coverage is the thing the four verdicts exist to prevent.
354
+
355
+ ## Requirements
356
+
357
+ Node 22 or later. One runtime dependency.
358
+
359
+ ## Corrections
360
+
361
+ If a finding is wrong, it is wrong in a way that matters, and a correction is more useful than a
362
+ polite silence.
@@ -0,0 +1,8 @@
1
+ {
2
+ "version": "0.1.0",
3
+ "bytes": 252644,
4
+ "externals": [
5
+ "ts-parser"
6
+ ],
7
+ "inputs": 43
8
+ }