diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 4f82169..f26fb0b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -13,7 +13,7 @@ A clear description of what you expected `lock()`, `close()`, or `yieldForAmount **Minimal reproduction** ```js -import { StakingService, InMemoryBalanceProvider, EXAMPLE_TIERS } from '@flashylabs/wdk-staking-kit' +import { StakingService, InMemoryBalanceProvider, EXAMPLE_TIERS } from '@flashyfinancial/wdk-staking-kit' const provider = new InMemoryBalanceProvider({ alice: '1000' }) const staking = new StakingService({ provider, tiers: EXAMPLE_TIERS }) @@ -25,7 +25,7 @@ The smaller this is, the faster it gets fixed. **Package version** -Output of `npm ls @flashylabs/wdk-staking-kit`. +Output of `npm ls @flashyfinancial/wdk-staking-kit`. **Environment** diff --git a/.github/workflows/deprecate-old-scope.yml b/.github/workflows/deprecate-old-scope.yml new file mode 100644 index 0000000..d1db665 --- /dev/null +++ b/.github/workflows/deprecate-old-scope.yml @@ -0,0 +1,52 @@ +name: Deprecate old npm scope + +# One-off, human-triggered. The package moved from @flashylabs/wdk-staking-kit +# to @flashyfinancial/wdk-staking-kit (finance packages move to the +# FlashyFinancial scope). This workflow runs `npm deprecate` against every +# version ever published under the old scope, pointing consumers at the new +# name — it does not touch the new scope, and it never runs on its own +# schedule. A human with the npm token triggers it, once, after confirming the +# new scope is actually live on the registry. +# +# Deliberately workflow_dispatch only: +# - no `push` trigger — this must never fire as a side effect of a commit +# - no `schedule` — deprecating is a one-time announcement, not a recurring job +# +# Uses the same NPM_TOKEN secret the publish workflow already uses. This +# workflow does not create, read, or need any other credential. + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + deprecate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 20 + registry-url: https://registry.npmjs.org + + - name: Verify the npm token can publish before doing anything else + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN_FLASHYOS_CI || secrets.NPM_TOKEN }} + run: | + if [ -z "$NODE_AUTH_TOKEN" ]; then + echo "::error::No npm token found. Add one with publish rights on the old @flashylabs scope as a repo secret named NPM_TOKEN_FLASHYOS_CI or NPM_TOKEN." + exit 1 + fi + if ! WHO=$(npm whoami 2>&1); then + echo "::error::The npm token is set but not valid for this scope — npm said: ${WHO}. Confirm this token has publish rights on the @flashylabs scope." + exit 1 + fi + echo "Authenticated to npm as ${WHO}" + + - name: Deprecate every version of the old scope's package + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN_FLASHYOS_CI || secrets.NPM_TOKEN }} + run: npm deprecate @flashylabs/wdk-staking-kit@* "moved to @flashyfinancial/wdk-staking-kit" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b8c355c..e76d66d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,6 +1,6 @@ name: Publish to npm -# Publishes @flashylabs/wdk-staking-kit when its version changes on main. +# Publishes @flashyfinancial/wdk-staking-kit when its version changes on main. # A version already on the registry is skipped rather than failing on # "cannot publish over the previously published version" — makes a re-run # idempotent. @@ -38,11 +38,11 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN_FLASHYOS_CI || secrets.NPM_TOKEN }} run: | if [ -z "$NODE_AUTH_TOKEN" ]; then - echo "::error::No npm token found. Add one with publish rights on the @flashylabs scope as a repo secret named NPM_TOKEN_FLASHYOS_CI or NPM_TOKEN." + echo "::error::No npm token found. Add one with publish rights on the @flashyfinancial scope as a repo secret named NPM_TOKEN_FLASHYOS_CI or NPM_TOKEN." exit 1 fi if ! WHO=$(npm whoami 2>&1); then - echo "::error::The npm token is set but not valid for this scope — npm said: ${WHO}. Confirm this token has publish rights on the @flashylabs scope." + echo "::error::The npm token is set but not valid for this scope — npm said: ${WHO}. Confirm this token has publish rights on the @flashyfinancial scope." exit 1 fi echo "Authenticated to npm as ${WHO}" @@ -51,12 +51,12 @@ jobs: id: check run: | V=$(node -p "require('./package.json').version") - if [ -n "$(npm view "@flashylabs/wdk-staking-kit@$V" version 2>/dev/null || true)" ]; then + if [ -n "$(npm view "@flashyfinancial/wdk-staking-kit@$V" version 2>/dev/null || true)" ]; then echo "publish=false" >> "$GITHUB_OUTPUT" - echo "::notice::@flashylabs/wdk-staking-kit@$V is already published." + echo "::notice::@flashyfinancial/wdk-staking-kit@$V is already published." else echo "publish=true" >> "$GITHUB_OUTPUT" - echo "::notice::@flashylabs/wdk-staking-kit@$V will be published." + echo "::notice::@flashyfinancial/wdk-staking-kit@$V will be published." fi - name: Test diff --git a/CHANGELOG.md b/CHANGELOG.md index b3c3ce8..576c1b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,9 @@ All notable changes to this project are documented here. Format loosely follows The published version is the one in [`package.json`](package.json); the topmost entry below must match it (a test pins this — see `test/doc-pins.test.mjs`), so the changelog can never quietly fall behind the package again. -## Unreleased +## 0.3.0 — 2026-10-30 + +**Changed (breaking):** the npm scope moved from `@flashylabs/wdk-staking-kit` to `@flashyfinancial/wdk-staking-kit` — this estate's brand-scoped reorg moves finance packages to the FlashyFinancial scope, the same move `@flashylabs/ledger` made to `@flashyos/ledger` at `1.0.1`. The package's name, every import and install instruction in the README and issue templates, the published manifest's `name`/`module.package` fields, and the publish workflow's scope now read `@flashyfinancial`. Nothing about the package's behaviour, its exports, or its GitHub repository location changed — `package.json`'s `repository`, `homepage` and `bugs` fields still name `github.com/FlashyLabs/wdk-staking-kit`, because this is a scope rename, not a repository move. A one-off, `workflow_dispatch`-only workflow, `.github/workflows/deprecate-old-scope.yml`, runs `npm deprecate @flashylabs/wdk-staking-kit@* "moved to @flashyfinancial/wdk-staking-kit"` against the old scope once a human with the npm token triggers it — it is not run automatically and was not run as part of this change. A consumer on `@flashylabs/wdk-staking-kit` must update their import and their `package.json` dependency; nothing on the old scope will receive this or any future release. **Fixed:** two `close()` calls for one matured position started together both fulfilled and `stake.closed` fired twice — the status check and the write sat either side of the provider's `credit()` await, the same shape as the lock defect fixed at 0.2.0. Only the provider's idempotency key on `staking-yield:${positionId}` prevented a second payment. `close()` now runs through the same per-holder chain as `lock()`, so exactly one close succeeds and the rest are refused `AlreadyClosedError`. Found by the harness below on the day it was wired, on the code that had just fixed the other three. diff --git a/INVARIANTS.md b/INVARIANTS.md index 050d6d5..3a38834 100644 --- a/INVARIANTS.md +++ b/INVARIANTS.md @@ -1,4 +1,4 @@ -# Invariants — `@flashylabs/wdk-staking-kit` +# Invariants — `@flashyfinancial/wdk-staking-kit` What this component guarantees about money, each enforced by a named piece of code and proved by a named test. The document is itself checked: diff --git a/README.md b/README.md index 28c47d6..8e3f3b3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# @flashylabs/wdk-staking-kit +# @flashyfinancial/wdk-staking-kit ``` ██ @@ -14,8 +14,8 @@ A reference staking primitive for wallets built on [Tether's WDK](https://github [![tests](https://github.com/FlashyLabs/wdk-staking-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/FlashyLabs/wdk-staking-kit/actions/workflows/ci.yml) [![CodeQL](https://github.com/FlashyLabs/wdk-staking-kit/actions/workflows/codeql.yml/badge.svg)](https://github.com/FlashyLabs/wdk-staking-kit/actions/workflows/codeql.yml) [![Scorecard](https://github.com/FlashyLabs/wdk-staking-kit/actions/workflows/scorecard.yml/badge.svg)](https://github.com/FlashyLabs/wdk-staking-kit/security/code-scanning) -[![npm version](https://img.shields.io/npm/v/@flashylabs/wdk-staking-kit.svg)](https://www.npmjs.com/package/@flashylabs/wdk-staking-kit) -[![npm downloads](https://img.shields.io/npm/dm/@flashylabs/wdk-staking-kit.svg)](https://www.npmjs.com/package/@flashylabs/wdk-staking-kit) +[![npm version](https://img.shields.io/npm/v/@flashyfinancial/wdk-staking-kit.svg)](https://www.npmjs.com/package/@flashyfinancial/wdk-staking-kit) +[![npm downloads](https://img.shields.io/npm/dm/@flashyfinancial/wdk-staking-kit.svg)](https://www.npmjs.com/package/@flashyfinancial/wdk-staking-kit) [![license](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](./LICENSE) [![node](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg)](https://nodejs.org) @@ -30,13 +30,13 @@ As of this package's first release, no WDK module offers staking — every walle ## Install ```bash -npm install @flashylabs/wdk-staking-kit +npm install @flashyfinancial/wdk-staking-kit ``` ## Quickstart ```js -import { StakingService, InMemoryBalanceProvider, EXAMPLE_TIERS } from '@flashylabs/wdk-staking-kit' +import { StakingService, InMemoryBalanceProvider, EXAMPLE_TIERS } from '@flashyfinancial/wdk-staking-kit' const provider = new InMemoryBalanceProvider({ alice: '100' }) const staking = new StakingService({ provider, tiers: EXAMPLE_TIERS }) @@ -63,7 +63,7 @@ const closed = await staking.close(position.id) `EXAMPLE_TIERS` is exactly that — an example. Publish your own: ```js -import { publishedTerms, checkTerms } from '@flashylabs/wdk-staking-kit' +import { publishedTerms, checkTerms } from '@flashyfinancial/wdk-staking-kit' const MY_TIERS = [ { id: 'flex-7', termDays: 7, aprBasisPoints: 150, minAmount: '10' }, @@ -131,12 +131,14 @@ const unsubscribe = staking.on('stake.closed', ({ position }) => { /* ... */ }) ## Status -Pre-1.0 (`0.2.0`). The tier and position shapes are not yet frozen. Watch [`CHANGELOG.md`](CHANGELOG.md) across a version bump before pinning a wider range than `^0.2.0`. +Pre-1.0 (`0.3.0`). The tier and position shapes are not yet frozen. Watch [`CHANGELOG.md`](CHANGELOG.md) across a version bump before pinning a wider range than `^0.3.0`. + +**The npm scope changed at `0.3.0`.** This package now publishes as `@flashyfinancial/wdk-staking-kit` — finance packages in this estate moved to the FlashyFinancial scope. `@flashylabs/wdk-staking-kit` will receive no further releases and is deprecated on the registry once a human with publish rights runs `deprecate-old-scope.yml`; update your import and your `package.json` dependency to the new name. ## Testing ```bash -npm test # 90 tests, node's built-in test runner, no external services +npm test # 102 tests, node's built-in test runner, no external services npm run check # confirms the generated manifest is current npm run invariants # INVARIANTS.md holds against the suite (invariants/1) ``` diff --git a/package-lock.json b/package-lock.json index 9d40248..02f56b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "@flashylabs/wdk-staking-kit", - "version": "0.1.3", + "name": "@flashyfinancial/wdk-staking-kit", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@flashylabs/wdk-staking-kit", - "version": "0.1.3", + "name": "@flashyfinancial/wdk-staking-kit", + "version": "0.3.0", "license": "Apache-2.0", "devDependencies": { "typescript": "^5.9.0" diff --git a/package.json b/package.json index 52b4546..6dc18ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "@flashylabs/wdk-staking-kit", - "version": "0.2.0", + "name": "@flashyfinancial/wdk-staking-kit", + "version": "0.3.0", "description": "A reference staking primitive for wallets built on Tether's WDK (or any wallet SDK): lock a balance for a fixed term at a published rate, without touching an on-chain contract or a ledger's internals \u2014 a lock earmarks a balance a provider already credits, and only yield is a real write, on close. No first-party WDK staking module exists; this is a generalized reference implementation, open for anyone to adopt or adapt.", "type": "module", "main": "./src/index.js", @@ -12,7 +12,7 @@ } }, "scripts": { - "test": "node --test test/balance-provider.test.mjs test/charter-conformance.test.mjs test/doc-pins.test.mjs test/invariants.test.mjs test/manifest.test.mjs test/mesh-refs.test.mjs test/staking.test.mjs test/strike.test.mjs test/terms.test.mjs test/test-script.test.mjs test/vendor-invariants.test.mjs test/workflow-pins.test.mjs", + "test": "node --test test/balance-provider.test.mjs test/charter-conformance.test.mjs test/doc-pins.test.mjs test/invariants.test.mjs test/manifest.test.mjs test/mesh-refs.test.mjs test/scope.test.mjs test/staking.test.mjs test/strike.test.mjs test/terms.test.mjs test/test-script.test.mjs test/vendor-invariants.test.mjs test/workflow-pins.test.mjs", "manifest": "node src/manifest.js --write", "check": "node src/manifest.js --check", "invariants": "node vendor-invariants.mjs check .", @@ -33,6 +33,10 @@ ], "author": "Flashy Labs", "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, "repository": { "type": "git", "url": "git+https://github.com/FlashyLabs/wdk-staking-kit.git" diff --git a/src/manifest.js b/src/manifest.js index 079b218..a2559d8 100644 --- a/src/manifest.js +++ b/src/manifest.js @@ -14,7 +14,7 @@ export function build() { const terms = publishedTerms({ tiers: EXAMPLE_TIERS, unit: 'example-unit', version: '0.1.0' }) return { contract: 'wdk-staking-kit-manifest/1', - name: '@flashylabs/wdk-staking-kit', + name: '@flashyfinancial/wdk-staking-kit', module: { package: pkg.name, version: pkg.version }, posture: { stage: 'balance-provider-first', diff --git a/test-types/consumer.ts b/test-types/consumer.ts index 7f30b9d..878a1c6 100644 --- a/test-types/consumer.ts +++ b/test-types/consumer.ts @@ -6,7 +6,7 @@ import { StakingService, InMemoryBalanceProvider, EXAMPLE_TIERS, tierById, yieldForAmount, checkTerms, type Tier, type BalanceProvider, -} from '@flashylabs/wdk-staking-kit' +} from '@flashyfinancial/wdk-staking-kit' // A plain object satisfying the BalanceProvider interface, not an instance // of InMemoryBalanceProvider — this is the exact case the interface exists diff --git a/test-types/tsconfig.json b/test-types/tsconfig.json index 2f2ea72..d8953ea 100644 --- a/test-types/tsconfig.json +++ b/test-types/tsconfig.json @@ -8,7 +8,7 @@ "skipLibCheck": true, "baseUrl": ".", "paths": { - "@flashylabs/wdk-staking-kit": ["../dist/index.d.ts"] + "@flashyfinancial/wdk-staking-kit": ["../dist/index.d.ts"] } }, "include": ["consumer.ts"] diff --git a/test/manifest.test.mjs b/test/manifest.test.mjs index 42e832a..3368b49 100644 --- a/test/manifest.test.mjs +++ b/test/manifest.test.mjs @@ -19,7 +19,7 @@ test('build(): matches the committed wdk-staking-kit.manifest.json — run `npm test('build(): contract id and module name', () => { const doc = build() assert.equal(doc.contract, 'wdk-staking-kit-manifest/1') - assert.equal(doc.module.package, '@flashylabs/wdk-staking-kit') + assert.equal(doc.module.package, '@flashyfinancial/wdk-staking-kit') }) test('build(): the embedded example terms pass their own offline verifier', () => { diff --git a/test/scope.test.mjs b/test/scope.test.mjs new file mode 100644 index 0000000..e11424d --- /dev/null +++ b/test/scope.test.mjs @@ -0,0 +1,103 @@ +// Pins the npm scope rename — @flashylabs/wdk-staking-kit moved to +// @flashyfinancial/wdk-staking-kit (finance packages move to the +// FlashyFinancial scope) — so the new name cannot quietly drift back to the +// old one in the places a consumer actually reads: the manifest, the +// published-facing README text, the type-level test's import, and the +// deprecation workflow that announces the move on the old scope. +// +// Deliberately NOT asserted here: `flashyos.roles.json`'s `repositories[].url` +// and `package.json`'s `repository`/`homepage`/`bugs` fields, which name the +// GitHub *repository location* (`github.com/FlashyLabs/wdk-staking-kit`) — +// a separate fact from the npm scope, and this is a scope rename, not a repo +// move. CHANGELOG.md's historical entries also legitimately keep the old +// scope, since they describe what shipped under it. +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const ROOT = dirname(dirname(fileURLToPath(import.meta.url))) +const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')) +const readme = readFileSync(join(ROOT, 'README.md'), 'utf8') +const bugReport = readFileSync(join(ROOT, '.github/ISSUE_TEMPLATE/bug_report.md'), 'utf8') +const consumer = readFileSync(join(ROOT, 'test-types/consumer.ts'), 'utf8') +const testTypesTsconfig = JSON.parse(readFileSync(join(ROOT, 'test-types/tsconfig.json'), 'utf8')) +const publishWorkflow = readFileSync(join(ROOT, '.github/workflows/publish.yml'), 'utf8') +const deprecateWorkflow = readFileSync(join(ROOT, '.github/workflows/deprecate-old-scope.yml'), 'utf8') + +const NEW_SCOPE = '@flashyfinancial/wdk-staking-kit' +const OLD_SCOPE = '@flashylabs/wdk-staking-kit' + +test('package.json publishes under the new scope', () => { + assert.equal(pkg.name, NEW_SCOPE) +}) + +test('package.json declares a public publishConfig', () => { + assert.equal(pkg.publishConfig?.access, 'public') + assert.equal(pkg.publishConfig?.registry, 'https://registry.npmjs.org/') +}) + +test('package.json\'s GitHub-facing fields are untouched by the scope rename', () => { + // This is a scope rename, not a repo move — the repository really does + // live at github.com/FlashyLabs/wdk-staking-kit. + assert.match(pkg.repository.url, /FlashyLabs\/wdk-staking-kit/) + assert.match(pkg.homepage, /FlashyLabs\/wdk-staking-kit/) + assert.match(pkg.bugs.url, /FlashyLabs\/wdk-staking-kit/) +}) + +test('README install command and import statements use the new scope', () => { + assert.ok(readme.includes(`npm install ${NEW_SCOPE}`), 'README install command still names the old scope') + const importLines = [...readme.matchAll(/from\s+'([^']+)'/g)].map((m) => m[1]) + assert.ok(importLines.length > 0, 'expected at least one import statement in README') + for (const spec of importLines) assert.equal(spec, NEW_SCOPE, `README imports from "${spec}"`) +}) + +test('README\'s npm badges point at the new scope', () => { + assert.ok(readme.includes(`img.shields.io/npm/v/${NEW_SCOPE}.svg`)) + assert.ok(readme.includes(`img.shields.io/npm/dm/${NEW_SCOPE}.svg`)) + assert.ok(readme.includes(`npmjs.com/package/${NEW_SCOPE}`)) +}) + +test('README does not name the old scope as something to install or import today', () => { + assert.ok(!readme.includes(`npm install ${OLD_SCOPE}`)) + assert.ok(!/from\s+'@flashylabs\/wdk-staking-kit'/.test(readme)) +}) + +test('the bug report template uses the new scope', () => { + assert.ok(bugReport.includes(`from '${NEW_SCOPE}'`)) + assert.ok(bugReport.includes(`npm ls ${NEW_SCOPE}`)) +}) + +test('the type-level consumer test imports the new scope, resolved through the matching tsconfig path', () => { + assert.match(consumer, /from\s+'@flashyfinancial\/wdk-staking-kit'/) + assert.ok(Object.keys(testTypesTsconfig.compilerOptions.paths).includes(NEW_SCOPE)) +}) + +test('the publish workflow targets the new scope', () => { + assert.ok(publishWorkflow.includes(`${NEW_SCOPE}@`)) + assert.ok(publishWorkflow.includes('@flashyfinancial scope')) + assert.ok(!publishWorkflow.includes(OLD_SCOPE)) +}) + +test('the deprecate-old-scope workflow is workflow_dispatch only, never scheduled or push-triggered', () => { + const lines = deprecateWorkflow.split('\n') + const onIndex = lines.findIndex((l) => l.trim() === 'on:') + assert.ok(onIndex !== -1, 'deprecate-old-scope.yml has no `on:` block') + const nextTopLevel = lines.findIndex((l, i) => i > onIndex && /^\S/.test(l)) + const onBlock = lines.slice(onIndex + 1, nextTopLevel === -1 ? undefined : nextTopLevel).join('\n') + assert.match(onBlock, /^\s*workflow_dispatch:\s*$/m, 'deprecate-old-scope.yml has no workflow_dispatch trigger') + assert.ok(!onBlock.includes('schedule:'), 'deprecate-old-scope.yml must never run on a schedule') + assert.ok(!onBlock.includes('push:'), 'deprecate-old-scope.yml must never run on push') +}) + +test('the deprecate-old-scope workflow runs exactly the documented npm deprecate command', () => { + assert.ok( + deprecateWorkflow.includes(`npm deprecate ${OLD_SCOPE}@* "moved to ${NEW_SCOPE}"`), + 'deprecate-old-scope.yml does not run the expected `npm deprecate` command', + ) +}) + +test('the deprecate-old-scope workflow reuses the existing npm secret rather than inventing a new one', () => { + assert.match(deprecateWorkflow, /secrets\.NPM_TOKEN_FLASHYOS_CI \|\| secrets\.NPM_TOKEN/) +}) diff --git a/wdk-staking-kit.manifest.json b/wdk-staking-kit.manifest.json index 4cd0d8f..6f4676e 100644 --- a/wdk-staking-kit.manifest.json +++ b/wdk-staking-kit.manifest.json @@ -1,9 +1,9 @@ { "contract": "wdk-staking-kit-manifest/1", - "name": "@flashylabs/wdk-staking-kit", + "name": "@flashyfinancial/wdk-staking-kit", "module": { - "package": "@flashylabs/wdk-staking-kit", - "version": "0.2.0" + "package": "@flashyfinancial/wdk-staking-kit", + "version": "0.3.0" }, "posture": { "stage": "balance-provider-first",