@column-na/card-reveal

中文文档
0.13.0发布于 2026/10/31SEE LICENSE IN LICENSETypeScriptES Module零依赖近 30 天下载 5520

Show a Column card's number, CVV, expiry and cardholder name on your web page without the data touching your code.

原始使用文档
文档目录14 节
  1. How it works
  2. Install
  3. Usage
  4. React
  5. Key-bound tokens: prepareReveal()
  6. Options
  7. ready , unmount , hide and show
  8. Copy, hide and show
  9. Errors
  10. Revealing the PIN
  11. Sandbox and live
  12. Content Security Policy
  13. Versioning
  14. License

@column-na/card-reveal

Show a Column card's full number, CVV, expiry and cardholder name on your web page. The card data never touches your code or your servers.

How it works

  1. The browser calls client.prepareReveal(). Column's frame at https://reveal.column.com makes the reveal's one-time key, and your page gets only its thumbprint, keyThumbprint. Send it to your server with the card ID.
  2. Your server creates a single-use reveal token, valid for one minute, bound to that key, using your API key:
    curl -X POST 'https://api.column.com/issuing/cards/<card_id>/reveal-tokens' -u :<YOUR API KEY> \
      -H 'Content-Type: application/json' -d '{"reveal_key_thumbprint": "<keyThumbprint>"}'
    # {"id":"crtk_...","card_id":"card_...","expires_at":"...","reveal_key_thumbprint":"..."}
    
    Check that the signed-in user owns the card before you do this. Then send only the token id to the browser.
  3. The browser calls prepared.reveal(...) with the card ID and token. Each value renders inside its own isolated iframe served by Column, so your page's JavaScript can't read it. The frame fetches the card details straight from Column's API, encrypted end to end to the one-time key, which never leaves the frame. The token works only with that key, so a token that leaks from your logs or your page is useless to anyone else.

Install

npm install @column-na/card-reveal

Or load the hosted script, pinned to an exact version with Subresource Integrity. Every version's tag, including its hash, is in that version's release notes:

<script
  src="https://reveal.column.com/<version>/card-reveal.js"
  integrity="sha384-..."
  crossorigin="anonymous"
></script>
<script>
  const client = window.ColumnCardReveal.createCardRevealClient({ environment: 'live' });
</script>

The hosted script refuses to create a client (integrity_required) unless its <script> tag has an integrity attribute holding one or more sha256-, sha384- or sha512- hashes, the values browsers verify. The SDK reads the attribute the way browsers do: hashes are separated by ASCII whitespace, and every one must be well-formed. A placeholder, an algorithm no browser checks, or a hash a browser can't parse (one followed by a non-breaking space pasted from a document, say) is refused the same way, because a browser skips what it can't parse and then loads the script with no check at all. Load it with a classic <script src> tag as above: <script type="module"> isn't supported, because a module script can't see its own tag and so can't tell that it was verified. Each version's file is published once and never changes. Versions up to 0.1.3 are at https://js.column.com/card-reveal/<version>/card-reveal.js, and stay there.

Usage

import { createCardRevealClient, CardRevealError } from '@column-na/card-reveal';

// Create one client and reuse it.
const client = createCardRevealClient({ environment: 'live' }); // or 'sandbox'

// One prepared reveal per token: makes the one-time key, and gives you its thumbprint to mint with.
const prepared = await client.prepareReveal();
const revealToken = await myServer.createRevealToken(cardId, prepared.keyThumbprint);

const { ready, unmount } = prepared.reveal({
  cardId,
  revealToken,
  fields: {
    pan: '#card-number',
    cvv2: '#cvv2',
    cardholder_name: '#name',
    expiration_month: '#exp-month',
    expiration_year: '#exp-year',
  },
  style: { color: '#fff', 'font-family': 'ui-monospace, monospace', 'font-size': '15px' },
  fieldStyles: { pan: { 'font-size': '19px' } },
  format: { pan: 'spaced', expirationYear: 'YY' },
  timeoutMs: 15000,
});

try {
  await ready;
} catch (error) {
  if (CardRevealError.is(error)) showMessage(error.code); // error.field and error.status when known
}

// Later, or when the component unmounts:
unmount();

In React, use the bindings below, or call reveal in an effect and return unmount as its cleanup. Each reveal needs a new token, because tokens are single use, and a new prepareReveal(), because each prepared key is used once.

React

@column-na/card-reveal/react does the prepare → mint → reveal → unmount sequence for you (React 18 or later):

import { CardRevealProvider, CardRevealSlot } from '@column-na/card-reveal/react';

<CardRevealProvider
  client={client}
  cardId={cardId}
  mintToken={({ cardId, keyThumbprint }) => myServer.createRevealToken(cardId, keyThumbprint)}
>
  <CardRevealSlot field="pan" style={{ width: 280, height: 30 }} />
  <CardRevealSlot field="cvv2" style={{ width: 60, height: 30 }} />
</CardRevealProvider>;
  • mintToken({ cardId, keyThumbprint }) asks your server for a token bound to the reveal's key and resolves with its id. It's called once per reveal.
  • <CardRevealSlot field="..."> is an empty <div> that one field's frame fills, so give it a width and height. It takes any div attribute.
  • <CardRevealProvider> also takes style, fieldStyles, format, timeoutMs and enabled (default true; false holds off, or hides a shown card).
  • Inside it, useCardRevealState() returns { status, error, retry }. status is 'idle', 'preparing', 'minting', 'revealing', 'ready' or 'error'. error is a CardRevealError (check with CardRevealError.is) or whatever mintToken threw. retry() starts over with a new key and token.
  • Changing the client, card, slots, styles, format or timeoutMs reveals again with a new token; unmounting the provider unmounts the reveal. A key prepared for a reveal that never happens (an unmount, or Strict Mode's double mount) is discarded.
  • useCardReveal({ client, cardId, mintToken, fields }) is the same as a hook, with fields mapping each field to a CSS selector, if you'd rather manage the containers yourself.

Key-bound tokens: prepareReveal()

client.prepareReveal() makes the next reveal's one-time P-256 key pair inside Column's frame, as a non-extractable WebCrypto key, before the token exists. It resolves with:

  • keyThumbprint: the key's RFC 7638 SHA-256 thumbprint, base64url without padding (43 characters). Pass it to your server, which mints the token with it as reveal_key_thumbprint. It's a hash of a public key, not a secret.
  • reveal(options): reveals with that key, with the same options and result as client.reveal(). It may be called once; a second call throws invalid_options. Invalid options throw before the key is used, so a corrected call can still use it.
  • discard(): drops the key without revealing and removes its hidden frame, for when the reveal won't happen (the token couldn't be minted, or the page moved on). Afterwards reveal() throws invalid_options.

Column answers a bound token only for a reveal that sends that key. Any other key gets a 401 (reveal_failed), and the token isn't used up. prepareReveal() rejects with script_load_failed, reveal_failed or timeout if the frame can't make the key. Until the reveal, the key waits in a hidden, zero-size frame at the end of <body>; the reveal removes it.

Every reveal token is minted with a reveal_key_thumbprint, so every reveal starts with prepareReveal(). client.reveal() takes the same options, but a token minted for another key, or a reveal made without the prepared key, gets a 401.

Options

OptionDescription
cardIdThe card's ID (card_...).
revealTokenA reveal token ID (crtk_...) from your server. Single use, valid for 1 minute.
fieldsMaps any of pan, cvv2, cardholder_name, expiration_month, expiration_year and pin to a CSS selector for its container. Include only the fields you want; see Revealing the PIN.
styleStyles for every field.
fieldStylesPer-field styles, applied on top of style.
format.pan'spaced' (default: 4111 1111 1111 1111 for 16-digit numbers) or 'plain'.
format.expirationYear'YYYY' (default) or 'YY'.
timeoutMsHow long ready waits before rejecting with timeout. Default 15000, maximum 120000.
copyabletrue, or per field ({ pan: true }): the field becomes a copy button inside Column's frame. See Copy, hide and show. Web only.
onCopyCalled with { field, copied } after each copy, where copied says whether the browser accepted it. Never the value.
hiddentrue reveals the fields masked (••••) until you call show(). Default false.
autoHideMsUnmounts the reveal this many milliseconds after ready resolves (1 to 3600000).
onAutoHideCalled once when autoHideMs unmounts the reveal.

Style objects accept CSS properties (kebab-case or camelCase), pseudo-class blocks such as '&:hover': { ... }, '@media (...)': { ... } and '@font-face': { ... }. The styles apply inside the iframe, and each container decides the iframe's size, so give your containers a width and height.

An '@font-face' rule's src must embed the font as a base64 data: URL (url(data:font/ttf;base64,...) format("truetype")), or name an installed font with local(...). A src that points at a URL is dropped and the iframe never fetches it: which fonts a frame loaded would tell that server which characters the card data contains. A style, including its embedded fonts, may be up to 256 KB as JSON.

An embedded font must be plain TrueType: an sfnt file (.ttf, signature 00 01 00 00 or true) made only of the cmap, glyf, head, hhea, hmtx, loca, maxp, name, post, OS/2 and gasp tables, whose every glyph is a simple outline (no composite glyphs) drawn within about 2 em of its origin (between -1 em and +2 em on each axis), with no advance width over 2 em, unhinted (no cvt , fpgm or prep table and no glyph instructions), at most 64 KB. WOFF and WOFF2, OpenType CFF (.otf), font collections, hinted fonts, and fonts with layout (GSUB, GPOS, GDEF), colour or variation tables don't pass. A font that doesn't pass is dropped silently, the same as an unsafe value, and the text renders in the next font of your font-family list, so list a fallback. Why: the page can time a reveal, and a font's size doesn't bound what painting it costs. A glyph may be drawn far outside its em box whatever the font-size, WOFF2 packs megabytes of outlines into kilobytes, a substitution feature expands one character into many glyphs, composite glyphs multiply points, and a hinting program is bytecode the browser's rasterizer runs for each glyph; each could make the time depend on which digits the value contains.

Preparing a font. A brand font as shipped usually carries kerning (GPOS), ligatures (GSUB) and hinting, and often composite glyphs, so export a copy for the reveal: plain TrueType, unhinted, no layout tables, only the characters you show. With fontTools:

pyftsubset brand.ttf --flavor= --no-hinting --layout-features='' --drop-tables+=GSUB,GPOS,GDEF,DSIG,kern --unicodes=U+0020-007E --output-file=brand-reveal.ttf

Then decompose any composite glyphs the subset keeps (fontTools' DecomposingRecordingPen replayed into a TTGlyphPen, as conformance/scripts/plain-fonts.py in the SDK repository does), check the file is at most 64 KB, and embed it as url(data:font/ttf;base64,...) format("truetype").

Styles are bounded, because the page can time a reveal and how long a value takes to paint must not depend on what it says. With a page-supplied font whose glyph for one digit is very expensive to paint, large text under blurred shadows took measurably longer when that digit was in the value, in every browser. So:

  • font-size is clamped to 96px (emitted as min(<your value>, 96px), whatever the unit), and the font shorthand, zoom, font-size-adjust and text-size-adjust are refused. The font-size keywords (large, larger, …) pass through.
  • text-shadow and box-shadow keep at most 2 shadows, each with a blur of at most 8px (given in px). A value with more shadows, a larger blur, or a var() or calc() in it is dropped altogether.
  • filter, backdrop-filter, transform (and transform-*, translate, rotate, scale, perspective), -webkit-text-stroke, text-emphasis, mask, paint-order and will-change are refused, as is -webkit-box-reflect. A vendor-prefixed spelling (-webkit-transform, -moz-transform, -webkit-filter, …) is treated as the property it aliases, so it is refused, clamped or bounded the same way.
  • Each embedded @font-face font may be at most 64 KB decoded and must be plain unhinted TrueType with simple glyphs inside their em box, as above; size-adjust is refused. Each glyph may have at most 600 points and 30 contours (a complex text glyph has under 400 and 30), and its outline may be at most 32 em long, measured along the axes (the sum of the horizontal and vertical distances between consecutive points around every contour): what a glyph costs to paint is how many scanlines its edges cross, a 64 KB font can hold a glyph of 30,000 points whose every edge spans the box, and even 600 such points are 1,800 em of edges. Every text glyph in a survey of macOS's TrueType fonts, the densest CJK glyphs of Songti and STHeiti included, is under 26 em; only swash ligatures, shade patterns and calligraphic Arabic ligatures exceed 32 em.
  • The frame never synthesizes bold or oblique and never computes skip-ink intersections (font-synthesis: none and text-decoration-skip-ink: none in its own stylesheet, which a style can't override), since both multiply the paint work per glyph. Declare a bold or italic face in @font-face if you want one; font-weight and font-style then pick it. An underline is drawn straight through descenders.
  • background-clip: text is refused, as the longhand (prefixed or not) and inside the background shorthand: it is one more pass through the glyph outlines on every paint. A var() in background-clip or background is refused too, since it could expand to text; write the value out.

A property outside these bounds is dropped silently, the way an unsafe value is; the reveal still succeeds.

ready, unmount, hide and show

  • ready resolves once every requested field has rendered.
  • If any field fails, or the timeout passes, ready rejects and every field is removed. A reveal shows all of its fields or none of them.
  • unmount() removes every field. If ready is still pending, it rejects with unmounted. A ready that already resolved stays resolved.
  • hide() and show() mask and unmask every field; see Copy, hide and show.
  • Invalid options throw invalid_options synchronously from reveal().

Copy, hide and show

const reveal = client.reveal({
  cardId,
  revealToken,
  fields: { pan: '#pan', cvv2: '#cvv2' },
  copyable: { pan: true },
  onCopy: ({ field, copied }) => showToast(copied ? 'Card number copied' : "Couldn't copy"),
  autoHideMs: 60_000,
  onAutoHide: () => showRevealButton(),
});

hideButton.onclick = () => reveal.hide();
showButton.onclick = () => reveal.show();
  • Copy. A copyable field is a button inside the frame (focusable, role="button", title "Copy"). A click, Enter or Space writes the value to the clipboard from inside the frame, so it never passes through your page; you get only onCopy({ field, copied }). A card number is copied without spaces. The copy control has the class copyable, so a style can target it: '&.copyable:hover', '&:focus-visible': { outline: '2px solid #0a84ff' }. Show your own confirmation from onCopy.
  • Browsers. Current Chrome, Edge, Firefox and Safari copy on click, Enter and Space. The SDK delegates clipboard-write to copyable frames only (allow="clipboard-write"), and falls back to document.execCommand('copy') inside the frame where the Clipboard API is unavailable or refused. If neither works, copied is false and nothing is written.
  • hide() and show() mask every field in place (each character but spaces shows as •, so the layout stays) and show it again, without a new token or request. A hidden field doesn't copy. Both do nothing after unmount() or a failure. hidden: true starts masked.
  • autoHideMs removes the fields that long after ready, as unmount() does: the values are gone, and showing them again needs a new token. ready stays resolved, and onAutoHide is called once. Use hide() instead if you want the cardholder to show them again.

Errors

Every error is a CardRevealError with a stable code. Use CardRevealError.is(error) to check, since it also works across separate copies of the SDK.

codeMeaning
invalid_optionsBad options (thrown synchronously), or no browser environment.
integrity_requiredThe hosted script's <script> tag has no integrity attribute with a sha256-, sha384- or sha512- hash, or it was loaded as a module script.
script_load_failedColumn's reveal frame couldn't load. The next reveal() retries.
container_not_foundNo element matches a field's selector. field is set.
reveal_failedThe reveal request failed. status is set when known, and apiCode when Column's API returned an error code. 401 means the token is missing, invalid, already used, expired, for a different card or bound to a different reveal key.
timeouttimeoutMs passed before every field rendered.
unmountedunmount() was called before the reveal finished.

Error messages never contain the token or any card data, and the SDK never logs either.

Revealing the PIN

Add pin to fields to show a debit card's PIN. The SDK asks Column only for the fields you list, so the PIN is returned only when you include pin, and a PIN-only reveal doesn't return the card number or CVV. Your server creates reveal tokens the same way either way. The card's program must have PIN reveal enabled. Verify the cardholder before you reveal their PIN.

Reveal pin on its own rather than alongside the card number. If the PIN can't be revealed, the whole reveal fails, so asking for it with other fields would hide those too. When it fails, reveal_failed carries Column's error code in apiCode:

apiCodeMeaning
pin_not_revealableThe PIN was set before PIN reveal was available. Set the PIN again to make it revealable.
feature_not_enabledPIN reveal isn't enabled for the card's program.
invalid_field_valueThe card has no PIN, or isn't a debit card.

These failures happen before the reveal token is used, so the same token still works for a reveal without pin.

Sandbox and live

LiveSandbox
DataReal cardsSimulated cards
RenderingOne iframe per fieldOne iframe per field
RequestLive-Mode: YesLive-Mode: No

Both render in Column's reveal frame, which fetches the card details from GET https://api.column.com/issuing/cards/{id}/reveal, encrypted end to end, so sandbox looks and behaves like live. In sandbox, sandboxBaseUrl can point the frame at a local mock: it must be https://api.column.com (the default) or plain http://localhost or http://127.0.0.1 with any port. Anything else, including https on loopback, throws invalid_options. renderer: 'column' is no longer needed and changes nothing.

Content Security Policy

Every reveal renders in Column's frame at https://reveal.column.com, and your page fetches nothing itself.

script-src  'self' https://reveal.column.com;
frame-src   https://reveal.column.com;
  • Drop https://reveal.column.com from script-src if you install from npm.
  • Your page needs no connect-src for the SDK, in sandbox or live, and no 'unsafe-inline'.

Versioning

This package follows semantic versioning, and the hosted script and React Native SDK release with the same version. Pin an exact version. Every request sends X-Column-SDK-Version: card-reveal-js/<version> so Column can tell which version you run.

License

Proprietary. Use is subject to the Column Card Reveal SDK License in LICENSE and requires an agreement with Column.

版本与发布记录

@column-na/card-reveal 共发布 14 个版本,首次发布于 2026/10/15;过去 12 个月发布了 14 个版本。

版本发布日期距今
0.13.0latest2026/10/3110 天前
0.12.02026/10/3011 天前
0.11.02026/10/3011 天前
0.10.02026/10/3011 天前
0.9.02026/10/2911 天前
0.8.02026/10/2912 天前

关于 @column-na/card-reveal 的常见问题

@column-na/card-reveal 是什么?

Show a Column card's number, CVV, expiry and cardholder name on your web page without the data touching your code.

如何安装 @column-na/card-reveal?

在项目目录运行 npm install @column-na/card-reveal,或使用 pnpm add / yarn add / bun add。

@column-na/card-reveal 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@column-na/card-reveal 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@column-na/card-reveal 可以商用吗?

@column-na/card-reveal 使用 SEE LICENSE IN LICENSE 许可。许可条款写在包内的独立文件中,请阅读该文件确认可否商用。以上为概括,不构成法律意见,请以许可原文为准。

@column-na/card-reveal 的下载量有多少?

根据 npm 官方下载统计,@column-na/card-reveal 在 2026/10/11 – 2026/11/09 这 30 天内被下载 5,520 次,日均约 184 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@column-na/card-reveal 还在维护吗?

最新版本 0.13.0 发布于 2026/10/31(10 天前);至今共发布 14 个版本,其中过去 12 个月发布了 14 个;首次发布于 2026/10/15。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。