Skip to content

Commit f48909d

Browse files
Merge pull request #9235 from BitGo/side/web-demo-profile-readme
docs(web-demo): add consumer config matrix for retail and UI
2 parents a14e296 + c38177f commit f48909d

7 files changed

Lines changed: 165 additions & 19 deletions

File tree

.github/workflows/ci.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ jobs:
127127
strategy:
128128
fail-fast: false
129129
matrix:
130-
check: ['lint', 'format', 'commit-lint', 'dependencies']
130+
check: ['lint', 'format', 'commit-lint', 'dependencies', 'scripts']
131131

132132
steps:
133133
- uses: socketdev/action@v1
@@ -174,6 +174,10 @@ jobs:
174174
if: matrix.check == 'dependencies'
175175
run: yarn run check-deps
176176

177+
- name: Test Root Scripts
178+
if: matrix.check == 'scripts'
179+
run: yarn run test:scripts
180+
177181
# We conciously do not audit dependencies as a PR step since errors are typically
178182
# unrelated to the PR changes. This check is performed in `publish.yml`.
179183

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ For modules that need browser support (especially those using `@bitgo/wasm-utxo`
7575
### Commits
7676
BitGoJS uses conventional commits. All commits MUST pass the spec described in `commitlint.config.js`.
7777

78+
Every commit needs a footer issue reference (`references-empty: never`). Web Experience side work without a Linear ticket may use `WEB-000` in the footer (junk-drawer reference, same pattern as `BTC-000` / `CSHLD-000`). Branches named `WEB-000-*` get `TICKET: WEB-000` appended automatically by `scripts/prepare-commit-msg.js`.
79+
7880
### TypeScript Guidelines
7981
- **Avoid `any` type**: Use specific types, interfaces, or union types instead of `any`. If absolutely necessary, prefer `unknown` and use type guards for safety.
8082

commitlint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ module.exports = {
7474
'WAL-',
7575
'WCN-',
7676
'WCI-',
77+
'WEB-',
7778
'COIN-',
7879
'COINS-',
7980
'COINFLP-',

modules/web-demo/README.md

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,63 @@
11
# web-demo
22

3-
This module serves as an example on how you can integrate the BitGo SDK into your own UI.
3+
Reference UI for integrating the BitGo SDK in the browser. Routes mirror problems solved in production apps (**bitgo-retail**, **bitgo-ui**) using SDK packages directly — no private `@bitgo-private/*` dependencies.
44

5-
## Usage
5+
## Quick start
66

7-
In order to start the server, run the following command after having already run `yarn install` in the root of the repo.
7+
From the BitGoJS repo root (after `yarn install`):
88

9-
```
9+
```bash
10+
cd modules/web-demo
1011
yarn dev
1112
```
1213

13-
This will spin up a server serving the UI at [localhost:8080](localhost:8080).
14+
Opens [localhost:8080](http://localhost:8080).
15+
16+
## Consumer config matrix
17+
18+
Production apps bundle the SDK differently. web-demo will support **consumer profiles** (via `CONSUMER_PROFILE` env) so each profile documents one real-world setup. Profiles are added incrementally; only `classic-webpack` is active today.
19+
20+
| Profile | Status | Mirrors | Bundler | WASM | SDK auth |
21+
| --- | --- | --- | --- | --- | --- |
22+
| `classic-webpack` | **active** (default) | bitgo-ui classic app, default web-demo | Webpack + [`webpack/bitgojs.config.js`](../../webpack/bitgojs.config.js) | Partial ESM aliases in shared config | Access token or `/webcrypto-auth` |
23+
| `retail-browser` | planned | bitgo-retail web (`MOBILE_MODE` off) | Webpack profile | Full `@bitgo/wasm-*` ESM paths, DKLS/cardano browser swaps | `WebCryptoHmacStrategy` + `requestIdPrefix` |
24+
| `retail-mobile-stub` | planned | bitgo-retail mobile webview | Webpack profile | Empty WASM shims (no heavy signing WASM) | Bridge / token (documented only) |
25+
| `direct-sdk` | planned | bitgo-retail mock server, `examples/ts` | Either | Per profile | `BitGoAPI` + `customRootURI`, no Express |
26+
| `express-proxy` | planned | Self-hosted Express users | Either | Per profile | Local Express signing proxy |
27+
28+
**Planned usage** (not wired yet):
29+
30+
```bash
31+
yarn dev # classic-webpack
32+
CONSUMER_PROFILE=retail-browser yarn dev # future
33+
CONSUMER_PROFILE=retail-mobile-stub yarn dev # future
34+
```
35+
36+
### What each production app solved
37+
38+
| Problem | bitgo-retail | bitgo-ui | web-demo route |
39+
| --- | --- | --- | --- |
40+
| Browser WASM loading | Vite ESM aliases for `@bitgo/wasm-utxo`, `wasm-ton`, `wasm-mps` | Webpack aliases (similar) | `/wasm-miniscript` |
41+
| HMAC without raw token in storage | `WebCryptoHmacStrategy` + IndexedDB | Classic token flow | `/webcrypto-auth` |
42+
| Passkey PRF wallet flows | `@bitgo-private/web-client` (retail); SDK: `@bitgo/passkey-crypto` | Partial | `/passkey-demo` |
43+
| Keycard generation | `@bitgo/key-card` in wallet create / migration | Keycard flows | `/keycard` |
44+
| Lazy coin registration | `retail-sdk-client/coinFactory` | `~/utils/coinFactory` | `/coins` |
45+
| Token enable prebuild | React Query + `PrebuildTransactionResult` | `useBuildTokenEnablementMutation` | planned `/token-enable` |
46+
47+
## Demo routes
48+
49+
| Path | Description |
50+
| --- | --- |
51+
| `/` | Home |
52+
| `/bitgo-js` | BitGo SDK object inspect |
53+
| `/bitgo-api` | BitGoAPI usage |
54+
| `/coins` | Coin factory / registration |
55+
| `/keycard` | Keycard download fixtures |
56+
| `/wasm-miniscript` | WASM miniscript smoke |
57+
| `/ecdsachallenge` | ECDSA challenge generation |
58+
| `/webcrypto-auth` | WebCrypto HMAC strategy + IndexedDB session |
59+
| `/passkey-demo` | Passkey register / attach / wallet (uses `@bitgo/passkey-crypto`) |
60+
61+
## Contributing
62+
63+
Owned by **@BitGo/web-experience** (`CODEOWNERS`). Prefer small PRs: one profile, one route, or one README section at a time. See the local BitGoJS side-improvements plan for micro-PR sizing.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,8 @@
278278
"precommit": "lint-staged",
279279
"lint-fix": "lerna run lint --parallel -- --fix",
280280
"prepare-commit-msg": "node ./scripts/prepare-commit-msg.js",
281-
"test:prepare-release": "mocha --require tsx ./scripts/tests/prepareRelease/prepare-release-main.test.ts"
281+
"test:prepare-release": "mocha --require tsx ./scripts/tests/prepareRelease/prepare-release-main.test.ts",
282+
"test:scripts": "mocha './scripts/tests/**/*.test.js'"
282283
},
283284
"dependencies": {
284285
"axios": "1.16.1",

scripts/prepare-commit-msg.js

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,58 @@ const childProcess = require('child_process');
55
const fs = require('fs');
66
const exec = promisify(childProcess.exec);
77

8-
// ex WP-1234
9-
const branchRegex = /([A-Z]+-)(\d+)/;
8+
// Build regex from commitlint's known issuePrefixes so arbitrary branch segments
9+
// (e.g. "release-2-hotfix") don't produce bogus TICKET footers.
10+
let issuePrefixes;
11+
try {
12+
issuePrefixes = require('../commitlint.config.js').parserPreset.parserOpts.issuePrefixes;
13+
} catch (_) {
14+
issuePrefixes = [];
15+
}
16+
const escapedPrefixes = issuePrefixes
17+
.filter((p) => p !== '#')
18+
.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
19+
// Anchored at branch start; case-insensitive so "web-000-desc" matches "WEB-"
20+
// null when no prefixes are configured — avoids ^()(\d+) matching bare-digit branches.
21+
const branchRegex =
22+
escapedPrefixes.length > 0 ? new RegExp(`^(${escapedPrefixes.join('|')})(\\d+)`, 'i') : null;
1023
// ex TICKET: WP-1234
1124
const commitRegex = /(ticket|issue):\s(\S+)/gim;
1225

26+
function extractTicket(branch) {
27+
if (!branchRegex) return null;
28+
const found = branch.match(branchRegex);
29+
return found ? found[0].toUpperCase() : null;
30+
}
31+
32+
// Given the existing commit message and a ticket, return the message with a
33+
// TICKET footer appended, or null when no change is needed (no ticket, or the
34+
// message already contains a ticket/issue footer). Pure so it is unit-testable.
35+
function appendTicketFooter(data, ticket) {
36+
if (!ticket) {
37+
return null;
38+
}
39+
// Reset lastIndex: commitRegex is global, so .test() is stateful across calls.
40+
commitRegex.lastIndex = 0;
41+
if (commitRegex.test(data)) {
42+
return null;
43+
}
44+
return `${data}\nTICKET: ${ticket}\n`;
45+
}
46+
1347
async function main() {
1448
const commitMsgFilepath = process.argv[2];
1549
try {
1650
const branch = (await exec(`git branch --show-current`)).stdout.trim();
17-
const found = branch.match(branchRegex);
18-
// Do not append message if branch name does not match regex
19-
if (!found.length) {
20-
return;
21-
}
22-
const ticket = found[0];
51+
const ticket = extractTicket(branch);
2352
const data = fs.readFileSync(commitMsgFilepath, 'utf8');
24-
// Exit if ticket is already in commit footer
25-
if (data.match(commitRegex)) {
53+
const updated = appendTicketFooter(data, ticket);
54+
if (updated === null) {
2655
return;
2756
}
28-
fs.writeFileSync(commitMsgFilepath, `${data}\nTICKET: ${ticket}\n`);
57+
fs.writeFileSync(commitMsgFilepath, updated);
2958
} catch (e) {}
3059
}
3160

32-
main();
61+
module.exports = { extractTicket, appendTicketFooter };
62+
if (require.main === module) main();
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const assert = require('assert');
2+
const { extractTicket, appendTicketFooter } = require('../prepare-commit-msg');
3+
4+
describe('extractTicket', () => {
5+
it('extracts uppercase ticket from uppercase branch', () => {
6+
assert.strictEqual(extractTicket('WEB-000-my-feature'), 'WEB-000');
7+
});
8+
9+
it('extracts and uppercases ticket from lowercase branch', () => {
10+
assert.strictEqual(extractTicket('web-000-my-feature'), 'WEB-000');
11+
});
12+
13+
it('extracts WP ticket', () => {
14+
assert.strictEqual(extractTicket('WP-1234-some-work'), 'WP-1234');
15+
});
16+
17+
it('returns null for branches without a known prefix', () => {
18+
assert.strictEqual(extractTicket('release-2-hotfix'), null);
19+
assert.strictEqual(extractTicket('main'), null);
20+
assert.strictEqual(extractTicket('feature-no-ticket'), null);
21+
});
22+
23+
it('is anchored at branch start (ignores prefixes mid-name)', () => {
24+
assert.strictEqual(extractTicket('feature/WEB-000-x'), null);
25+
});
26+
27+
it('returns null for prefix without digits (WEB-foo)', () => {
28+
assert.strictEqual(extractTicket('WEB-foo'), null);
29+
});
30+
31+
it('returns null for empty branch', () => {
32+
assert.strictEqual(extractTicket(''), null);
33+
});
34+
});
35+
36+
describe('appendTicketFooter', () => {
37+
it('appends a TICKET footer when none is present', () => {
38+
assert.strictEqual(appendTicketFooter('feat: add thing', 'WEB-000'), 'feat: add thing\nTICKET: WEB-000\n');
39+
});
40+
41+
it('returns null when ticket is null (branch had no match)', () => {
42+
assert.strictEqual(appendTicketFooter('feat: add thing', null), null);
43+
});
44+
45+
it('returns null when a TICKET footer already exists', () => {
46+
assert.strictEqual(appendTicketFooter('feat: add thing\n\nTICKET: WEB-000', 'WEB-000'), null);
47+
});
48+
49+
it('returns null when an ISSUE footer already exists (any case)', () => {
50+
assert.strictEqual(appendTicketFooter('feat: add thing\n\nissue: WEB-111', 'WEB-000'), null);
51+
});
52+
53+
it('is not stateful across repeated calls (global-regex lastIndex safe)', () => {
54+
// Guards against RegExp.lastIndex leaking between .test() invocations.
55+
assert.strictEqual(appendTicketFooter('feat: a', 'WEB-000'), 'feat: a\nTICKET: WEB-000\n');
56+
assert.strictEqual(appendTicketFooter('feat: b', 'WEB-000'), 'feat: b\nTICKET: WEB-000\n');
57+
});
58+
});

0 commit comments

Comments
 (0)