Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
16ce231
DES-21: Pagination Base UI idiom upgrades + optional totalItems (#26918)
jaymantri Apr 30, 2026
aefb119
[grid] add typed wrapper for Origin Button (#26770)
jaymantri Apr 30, 2026
1e24a82
DES-23: Add LoadMore infinite-scroll primitive + useLoadMore hook (#2…
jaymantri Apr 30, 2026
497e6f9
fix(origin): unblock site builds — LoadMoreTriggerProps render overri…
jaymantri Apr 30, 2026
62d30b8
CI update lock file for PR
May 1, 2026
6f9cae0
[grid] Example app to test wallet module (#26717)
carsonp6 May 1, 2026
6f0e85a
CI update lock file for PR
May 1, 2026
4d67f34
[origin] add scoped globals for mixed app routes (#26900)
coreymartin May 1, 2026
de5ffdb
feat(origin/BarChart): anchor non-stacked bars at value 0 when 0 is i…
jamesxu-lightspark May 1, 2026
23405ed
[site] render auth buttons with Origin (#26933)
coreymartin May 1, 2026
4d66177
[js] Restore tests to CI checks (#26962)
coreymartin May 2, 2026
87a690d
[origin] Deploy Storybook to dev.dev.sparkinfra.net (#26994)
coreymartin May 4, 2026
885ae3a
CI update lock file for PR
May 4, 2026
60d92b5
[origin] Add Storybook PR previews (#27001)
coreymartin May 4, 2026
af6bb21
[origin] Lower scoped reset specificity (#27028)
jaymantri May 5, 2026
64a04d8
Simplify Combobox: align with Base UI multi-select pattern (DES-24) (…
jaymantri May 6, 2026
e80a390
[vite] Handle stale dev proxy ALB sessions (#27087)
coreymartin May 6, 2026
f61fff1
Migrate ops DLQ to DataManagerTable (#27080)
bsiaotickchong May 6, 2026
90146eb
[origin] Expose Field root render prop (#27097)
jaymantri May 6, 2026
7a2625d
[origin] Simplify Combobox chip composition (#27141)
jaymantri May 7, 2026
d6887ed
[origin] Add Field label suffix spacing (#27140)
jaymantri May 7, 2026
7046649
[origin] Bound Select popup height (#27142)
jaymantri May 7, 2026
8cf11b0
[origin] Add ButtonLink for link semantics (#27144)
jaymantri May 7, 2026
a3f6b53
[origin] Make Tabs panel unopinionated (#27143)
jaymantri May 7, 2026
27d4aac
DES-22: Add `Pager` direction-based pagination primitive (#26921)
jaymantri May 7, 2026
766561b
[js] Increase npm dependency age gate (#27190)
coreymartin May 8, 2026
e355ae1
[site] surface USDT-Tron in the Grid dashboard payout flow (#27211)
jklein24 May 14, 2026
ba1130e
[site] Speed up local Playwright UI tests (#25379)
coreymartin May 19, 2026
a5a2c99
[origin] Add form composition boundary tests (#27119)
jaymantri May 20, 2026
b2b4f09
fix(treasury): fix symbol case, restructure columns, fix stablecoin f…
k15z May 21, 2026
8cdb5ab
[uma-nage] Add segmented navigation wrapper (#27106)
jaymantri May 22, 2026
9cd70ea
[ui] Use built package imports in private apps (#27024)
coreymartin May 22, 2026
1128b0f
Fix fail-open async UMA validation checks (#524)
SOME-1HING May 25, 2026
32792ca
[js] Update form-data resolution (#27827)
coreymartin May 26, 2026
5bc0a0d
CI update lock file for PR
May 26, 2026
dfc6a8c
[js] Update vite dependency (#27876)
coreymartin May 27, 2026
9c2a3f1
[origin] Update fast-uri dependency (#27874)
coreymartin May 27, 2026
ae2b584
[js] Update axios dependency (#27873)
coreymartin May 27, 2026
1508dc1
CI update lock file for PR
May 27, 2026
b17529c
[js] Update react-router-dom dependency (#27861)
coreymartin May 27, 2026
37493d8
CI update lock file for PR
May 27, 2026
729ded5
DEMO(grid): add internal demo app for hosted KYC/KYB link API (#27615)
jklein24 May 28, 2026
3ae133b
Fix js build by running yarn (#27939)
jklein24 May 28, 2026
795107e
CI update lock file for PR
May 28, 2026
097aee9
DEMO(grid): add embedded Sumsub WebSDK option to demo (#27726)
jklein24 May 28, 2026
d908f6c
Update docs link domains (#27917)
bsiaotickchong May 28, 2026
c78e1b2
[js] Upgrade to React 19 and Next.js 15 (#27901)
coreymartin May 29, 2026
4c18497
CI update lock file for PR
May 29, 2026
2b81c87
[js] Update nodemon dependency (#27856)
coreymartin May 30, 2026
ad4d94f
CI update lock file for PR
May 30, 2026
f680742
[js] Update minimatch dependency (#27849)
coreymartin May 30, 2026
1e16384
CI update lock file for PR
May 30, 2026
6c742e4
[js][gga] Update example app (#28004)
carsonp6 Jun 1, 2026
e784c0a
[js] Update js-cookie dependency (#27866)
coreymartin Jun 1, 2026
fc30cb0
[js] Update Vitest to v4 (#28092)
coreymartin Jun 1, 2026
2c7d6ae
CI update lock file for PR
Jun 1, 2026
5f4cff3
[ui] Build icons from a single entrypoint (#28172)
coreymartin Jun 3, 2026
45b334c
[docs] Replace remark-prism with rehype-prism-plus (#28163)
coreymartin Jun 3, 2026
a2ccd0e
CI update lock file for PR
Jun 3, 2026
581a2f8
Gatekeeper: keyboard row navigation + highlight for the results table…
akanter Jun 4, 2026
821c5e3
[grid] Add read-only Home foundation (#27949)
jaymantri Jun 4, 2026
a327801
[grid] add CNY mobile-wallet payout corridor (AliPay / WeChatPay) (#2…
mohamedwane Jun 4, 2026
1eabc3a
[js] Upgrade libphonenumber-js (#28416)
coreymartin Jun 8, 2026
2b82edb
CI update lock file for PR
Jun 8, 2026
74d4fc8
fix(origin): thin x-axis labels by measured width (#28120)
jamesxu-lightspark Jun 9, 2026
f7e42c2
[origin] Fix long-list dropdown scrolling (#28492)
jaymantri Jun 9, 2026
7ad22a7
[origin] Default combobox clear to active (#28544)
jaymantri Jun 11, 2026
ddf20c0
chore(js): remove unused static logo asset (#28518)
kphurley7 Jun 11, 2026
779ce90
DES-51: add Nage phone input foundation (#28445)
jaymantri Jun 11, 2026
4bd38aa
[site] Fix preview login redirects (#28555)
coreymartin Jun 11, 2026
909a749
[grid] Add Receive/Add Funds money-flow primitives (#27951)
jaymantri Jun 11, 2026
a3b7643
[js] gga example app: V3 secure OTP e2e flow (test harness) (#28154)
carsonp6 Jun 12, 2026
d256b2d
CI update lock file for PR
Jun 12, 2026
004dae2
[js] gga example app: real WebAuthn ceremony + OTP-session caching; e…
carsonp6 Jun 12, 2026
d22cd61
[js] gga example app: split main.ts into config/turnkey/webauthn/api-…
carsonp6 Jun 12, 2026
42dc5e3
[js] gga example app: session.ts + status chip; disable-with-tooltip …
carsonp6 Jun 12, 2026
1ff29c6
[js] gga example app: prune dead flows + stale PR-number comments (#2…
carsonp6 Jun 12, 2026
9b760c4
[js] gga example app: sandbox/production mode split driven by SANDBOX…
carsonp6 Jun 12, 2026
3eb08af
[js] gga example app: guided login/manage flows + Advanced (manual) t…
carsonp6 Jun 12, 2026
88d6ef4
Add shellcheck linting across the repo (#28620)
markatto Jun 12, 2026
2c02a0d
feat(gga): redesign example app as React + Origin (Platform/Customer …
carsonp6 Jun 12, 2026
8fcbeab
CI update lock file for PR
Jun 12, 2026
0c0c505
[js] Upgrade Turbo for worktree cache sharing (#28807)
coreymartin Jun 13, 2026
657cc5f
Enable global Yarn cache (#28798)
coreymartin Jun 13, 2026
d297d00
CI update lock file for PR
Jun 13, 2026
dabbef1
[origin] Fix sidebar navigation roles (#28838)
jaymantri Jun 15, 2026
c7c3755
feat(gga): customer transactions tab (real Grid history) (#28761)
carsonp6 Jun 16, 2026
f16158d
[gga][example] Remove md files (#28995)
carsonp6 Jun 17, 2026
97933dc
[grid] Normalize crypto network and token icons (#29044)
jaymantri Jun 18, 2026
1010f42
Add sorting to treasury flows table (#29232)
k15z Jun 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ test-config.yml
dist/
build/
lib/
# The GGA example app keeps decoupled source modules under src/lib/ (not a
# compiled-output dir), so it must not be swept up by the `lib/` rule above.
!apps/examples/grid-global-accounts-example-app/src/lib/

# Vim swap files
*.swp
Expand Down Expand Up @@ -126,3 +129,6 @@ stats.html

# Dev proxy cookies (contains ALB session tokens)
.dev-proxy-cookies

# Playwright MCP logs
.playwright-mcp/
4 changes: 2 additions & 2 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
compressionLevel: mixed

enableGlobalCache: false
enableGlobalCache: true

nodeLinker: node-modules

npmMinimalAgeGate: 1440
npmMinimalAgeGate: 4320 # 3 days

npmPreapprovedPackages:
- "@lightsparkdev/*"
Expand Down
2 changes: 2 additions & 0 deletions apps/examples/grid-global-accounts-example-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Verification screenshots — captured locally, not tracked.
.screenshots/
12 changes: 12 additions & 0 deletions apps/examples/grid-global-accounts-example-app/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Grid Global Accounts</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
30 changes: 30 additions & 0 deletions apps/examples/grid-global-accounts-example-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@lightsparkdev/grid-global-accounts-example-app",
"private": true,
"version": "0.0.1",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"start": "vite",
"preview": "vite preview",
"test": "vitest run"
},
"devDependencies": {
"@types/react": "^19.2.15",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.2.0",
"typescript": "^5.6.2",
"vite": "^8.0.14",
"vitest": "^4.1.7"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@lightsparkdev/origin": "*",
"@turnkey/api-key-stamper": "^0.6.5",
"@turnkey/crypto": "^2.8.14",
"@turnkey/encoding": "^0.6.0",
"react": "^19.2.6",
"react-dom": "^19.2.6"
}
}
19 changes: 19 additions & 0 deletions apps/examples/grid-global-accounts-example-app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Shell } from "./components/Shell";
import { AppStateProvider, useAppState } from "./state/store";
import { CustomerView } from "./views/customer/CustomerView";
import { PlatformView } from "./views/platform/PlatformView";

export function App() {
return (
<AppStateProvider>
<Shell>
<Router />
</Shell>
</AppStateProvider>
);
}

function Router() {
const { persona } = useAppState();
return persona === "platform" ? <PlatformView /> : <CustomerView />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { afterEach, describe, expect, it } from "vitest";

import {
clearActiveSession,
getAccountId,
getSessionId,
getSessionModel,
hasSessionSigningKey,
resolveSessionKeys,
setAccountId,
setActiveSessionAccount,
setSessionId,
setSessionKeysFromTek,
} from "../session";

// Reset to logged-out between tests so module-level context state can't leak.
afterEach(() => {
setActiveSessionAccount(null);
});

describe("per-customer session isolation", () => {
it("keeps signing keys, model, and account id independent per account key", () => {
// No active context → logged-out, getters return empty/null.
setActiveSessionAccount(null);
expect(hasSessionSigningKey()).toBe(false);
expect(resolveSessionKeys()).toBeNull();
expect(getAccountId()).toBe("");
expect(getSessionModel()).toBe("none");

// Customer A signs in (OTP-TEK model) under its own account key.
setActiveSessionAccount("InternalAccount:A");
setAccountId("InternalAccount:A");
setSessionKeysFromTek({ publicKey: "pubA", privateKey: "privA" });
expect(hasSessionSigningKey()).toBe(true);
expect(getSessionModel()).toBe("otp-tek");
expect(resolveSessionKeys()).toEqual({
apiPublicKey: "pubA",
apiPrivateKey: "privA",
});
expect(getAccountId()).toBe("InternalAccount:A");

// Switch to a fresh customer B: it inherits NOTHING from A.
setActiveSessionAccount("InternalAccount:B");
expect(hasSessionSigningKey()).toBe(false);
expect(resolveSessionKeys()).toBeNull();
expect(getSessionModel()).toBe("none");
expect(getAccountId()).toBe("");

// B establishes its own session with different keys.
setAccountId("InternalAccount:B");
setSessionKeysFromTek({ publicKey: "pubB", privateKey: "privB" });
expect(resolveSessionKeys()).toEqual({
apiPublicKey: "pubB",
apiPrivateKey: "privB",
});
expect(getAccountId()).toBe("InternalAccount:B");

// Switching back to A restores A's cached session, untouched by B.
setActiveSessionAccount("InternalAccount:A");
expect(hasSessionSigningKey()).toBe(true);
expect(getSessionModel()).toBe("otp-tek");
expect(resolveSessionKeys()).toEqual({
apiPublicKey: "pubA",
apiPrivateKey: "privA",
});
expect(getAccountId()).toBe("InternalAccount:A");
});

it("treats null as logged-out with no active context", () => {
setActiveSessionAccount("InternalAccount:A");
setSessionKeysFromTek({ publicKey: "pubA", privateKey: "privA" });
expect(hasSessionSigningKey()).toBe(true);

setActiveSessionAccount(null);
expect(hasSessionSigningKey()).toBe(false);
expect(resolveSessionKeys()).toBeNull();
expect(getAccountId()).toBe("");

// Mutators no-op while logged-out; later re-activation still has A cached.
setSessionKeysFromTek({ publicKey: "pubX", privateKey: "privX" });
setActiveSessionAccount("InternalAccount:A");
expect(resolveSessionKeys()).toEqual({
apiPublicKey: "pubA",
apiPrivateKey: "privA",
});
});

it("clearActiveSession wipes the active context's signing key without touching others", () => {
// Customer A signs in under its own account key.
setActiveSessionAccount("InternalAccount:clearA");
setAccountId("InternalAccount:clearA");
setSessionId("session-A");
setSessionKeysFromTek({ publicKey: "pubA", privateKey: "privA" });

// Customer B signs in under a different key.
setActiveSessionAccount("InternalAccount:clearB");
setAccountId("InternalAccount:clearB");
setSessionId("session-B");
setSessionKeysFromTek({ publicKey: "pubB", privateKey: "privB" });

// Back on A, clearing wipes A's signing key/model/session id but keeps the
// account id so the slot still belongs to A (logged out, can re-auth).
setActiveSessionAccount("InternalAccount:clearA");
expect(hasSessionSigningKey()).toBe(true);
clearActiveSession();
expect(hasSessionSigningKey()).toBe(false);
expect(resolveSessionKeys()).toBeNull();
expect(getSessionModel()).toBe("none");
expect(getSessionId()).toBe("");
expect(getAccountId()).toBe("InternalAccount:clearA");

// B is untouched by clearing A.
setActiveSessionAccount("InternalAccount:clearB");
expect(hasSessionSigningKey()).toBe(true);
expect(getSessionModel()).toBe("otp-tek");
expect(getSessionId()).toBe("session-B");
expect(resolveSessionKeys()).toEqual({
apiPublicKey: "pubB",
apiPrivateKey: "privB",
});
});

it("clearActiveSession is a no-op when logged out", () => {
setActiveSessionAccount(null);
expect(() => clearActiveSession()).not.toThrow();
expect(hasSessionSigningKey()).toBe(false);
});

it("preserves first-account-wins for setAccountId within a fresh context", () => {
// A distinct key so the context is fresh (contexts persist across tests).
setActiveSessionAccount("InternalAccount:C");
setAccountId("first");
setAccountId("second");
expect(getAccountId()).toBe("first");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { describe, expect, it } from "vitest";

import {
buildAllowCredentials,
buildAssertionOptions,
buildCreationOptions,
bytesToB64Url,
SECURITY_KEY_TRANSPORTS,
} from "../webauthn";

const RP = "localhost";

describe("buildCreationOptions — forces a cross-platform security key", () => {
const challenge = new Uint8Array([1, 2, 3]);
const userId = new Uint8Array([9, 9]);
const opts = buildCreationOptions("My key", RP, challenge, userId);

it("requests a cross-platform (roaming) authenticator, not the platform one", () => {
expect(opts.authenticatorSelection?.authenticatorAttachment).toBe(
"cross-platform",
);
});

it("uses security-key-friendly resident-key + UV settings", () => {
expect(opts.authenticatorSelection?.residentKey).toBe("discouraged");
expect(opts.authenticatorSelection?.requireResidentKey).toBe(false);
expect(opts.authenticatorSelection?.userVerification).toBe("preferred");
});

it("offers ES256 (-7) in pubKeyCredParams", () => {
expect(opts.pubKeyCredParams).toContainEqual({
type: "public-key",
alg: -7,
});
});

it("sets the rp id and passes the challenge/user through", () => {
expect(opts.rp.id).toBe(RP);
expect(opts.challenge).toBe(challenge);
expect(opts.user.id).toBe(userId);
});
});

describe("buildAllowCredentials — targets the security key over USB/NFC", () => {
// A valid base64url credential id (decodes cleanly via atob).
const idA = bytesToB64Url(new Uint8Array([10, 20, 30]));
const idB = bytesToB64Url(new Uint8Array([40, 50, 60]));

it("includes every registered id with usb/nfc transports", () => {
const out = buildAllowCredentials([idA, idB]);
expect(out).toHaveLength(2);
for (const d of out) {
expect(d.type).toBe("public-key");
expect(d.transports).toEqual(SECURITY_KEY_TRANSPORTS);
expect(d.transports).toEqual(["usb", "nfc"]);
}
});

it("drops blank and duplicate ids", () => {
const out = buildAllowCredentials([idA, "", " ", idA]);
expect(out).toHaveLength(1);
});

it("returns [] when no ids are known (discoverable-credential fallback)", () => {
expect(buildAllowCredentials([])).toEqual([]);
});
});

describe("buildAssertionOptions", () => {
const challenge = new Uint8Array([7]);
const id = bytesToB64Url(new Uint8Array([1, 2, 3, 4]));

it("wires the rp id, challenge, UV and the allowCredentials", () => {
const opts = buildAssertionOptions(challenge, [id], RP);
expect(opts.rpId).toBe(RP);
expect(opts.challenge).toBe(challenge);
expect(opts.userVerification).toBe("preferred");
expect(opts.allowCredentials).toHaveLength(1);
expect(opts.allowCredentials?.[0].transports).toEqual(["usb", "nfc"]);
});

it("yields an empty allowCredentials when no ids are known", () => {
const opts = buildAssertionOptions(challenge, [], RP);
expect(opts.allowCredentials).toEqual([]);
});
});
Loading
Loading