Skip to content

Commit 0a7c4fd

Browse files
stainless-botrattrayalex
authored andcommitted
Add client.cards.get_embed_url() and client.cards.get_embed_html().
1 parent a72cb63 commit 0a7c4fd

9 files changed

Lines changed: 315 additions & 7 deletions

File tree

lithic/_base_client.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import anyio
2525
import httpx
2626
import pydantic
27+
from httpx import URL
2728
from pydantic import PrivateAttr
2829

2930
from . import _base_exceptions as exceptions
@@ -349,6 +350,10 @@ def default_headers(self) -> Dict[str, str]:
349350
def user_agent(self) -> str:
350351
return f"{self.__class__.__name__}/Python {self._version}"
351352

353+
@property
354+
def base_url(self) -> URL:
355+
return self._client.base_url
356+
352357
@lru_cache(maxsize=None)
353358
def platform_properties(self) -> str:
354359
arch, _ = platform.architecture()

lithic/resources/cards.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
# File generated from our OpenAPI spec by Stainless.
22

3+
import hmac
4+
import json
5+
import base64
6+
import hashlib
37
from typing import Union
8+
from datetime import datetime, timezone, timedelta
9+
10+
from httpx import URL
411

512
from .._types import NOT_GIVEN, Headers, Timeout, NotGiven
613
from .._resource import SyncAPIResource, AsyncAPIResource
@@ -12,8 +19,11 @@
1219
from ..types.card_create_params import CardCreateParams
1320
from ..types.card_update_params import CardUpdateParams
1421
from ..types.card_reissue_params import CardReissueParams
22+
from ..types.embed_request_param import *
1523
from ..types.card_provision_params import CardProvisionParams
1624
from ..types.card_provision_response import *
25+
from ..types.card_get_embed_url_params import CardGetEmbedURLParams
26+
from ..types.card_get_embed_html_params import CardGetEmbedHTMLParams
1727

1828
__all__ = ["Cards", "AsyncCards"]
1929

@@ -144,6 +154,80 @@ def embed(
144154
cast_to=str,
145155
)
146156

157+
def get_embed_html(
158+
self,
159+
query: CardGetEmbedHTMLParams,
160+
*,
161+
headers: Union[Headers, NotGiven] = NOT_GIVEN,
162+
max_retries: Union[int, NotGiven] = NOT_GIVEN,
163+
timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN,
164+
) -> str:
165+
"""
166+
Generates and executes an embed request, returning html you can serve to the
167+
user.
168+
169+
Be aware that this html contains sensitive data whose presence on your server
170+
could trigger PCI DSS.
171+
172+
If your company is not certified PCI compliant, we recommend using
173+
`get_embed_url()` instead. You would then pass that returned URL to the
174+
frontend, where you can load it via an iframe.
175+
"""
176+
headers = {"Accept": "text/html", **(headers or {})}
177+
return self._get(
178+
str(self.get_embed_url(query)),
179+
cast_to=str,
180+
options=make_request_options(headers, max_retries, timeout),
181+
)
182+
183+
def get_embed_url(self, query: CardGetEmbedURLParams) -> URL:
184+
"""
185+
Handling full card PANs and CVV codes requires that you comply with the Payment
186+
Card Industry Data Security Standards (PCI DSS). Some clients choose to reduce
187+
their compliance obligations by leveraging our embedded card UI solution
188+
documented below.
189+
190+
In this setup, PANs and CVV codes are presented to the end-user via a card UI
191+
that we provide, optionally styled in the customer's branding using a specified
192+
css stylesheet. A user's browser makes the request directly to api.lithic.com,
193+
so card PANs and CVVs never touch the API customer's servers while full card
194+
data is displayed to their end-users. The response contains an HTML document.
195+
This means that the url for the request can be inserted straight into the `src`
196+
attribute of an iframe.
197+
198+
```html
199+
<iframe
200+
id="card-iframe"
201+
src="https://sandbox.lithic.com/v1/embed/card?embed_request=eyJjc3MiO...;hmac=r8tx1..."
202+
allow="clipboard-write"
203+
class="content"
204+
></iframe>
205+
```
206+
207+
You should compute the request payload on the server side. You can render it (or
208+
the whole iframe) on the server or make an ajax call from your front end code,
209+
but **do not ever embed your API key into front end code, as doing so introduces
210+
a serious security vulnerability**.
211+
"""
212+
# Default expiration of 1 minute from now.
213+
query.setdefault("expiration", (datetime.now(timezone.utc) + timedelta(minutes=1)).isoformat())
214+
serialized = json.dumps(query, sort_keys=True, separators=(",", ":"))
215+
params = {
216+
"embed_request": base64.b64encode(bytes(serialized, "utf-8")).decode("utf-8"),
217+
"hmac": base64.b64encode(
218+
hmac.new(
219+
key=bytes(self._client.api_key, "utf-8"),
220+
msg=bytes(serialized, "utf-8"),
221+
digestmod=hashlib.sha256,
222+
).digest()
223+
).decode("utf-8"),
224+
}
225+
226+
# Copied nearly directly from httpx.BaseClient._merge_url
227+
base_url = self._client.base_url
228+
raw_path = base_url.raw_path + URL("embed/card").raw_path
229+
return base_url.copy_with(raw_path=raw_path).copy_merge_params(params)
230+
147231
def provision(
148232
self,
149233
card_token: str,
@@ -317,6 +401,80 @@ async def embed(
317401
cast_to=str,
318402
)
319403

404+
async def get_embed_html(
405+
self,
406+
query: CardGetEmbedHTMLParams,
407+
*,
408+
headers: Union[Headers, NotGiven] = NOT_GIVEN,
409+
max_retries: Union[int, NotGiven] = NOT_GIVEN,
410+
timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN,
411+
) -> str:
412+
"""
413+
Generates and executes an embed request, returning html you can serve to the
414+
user.
415+
416+
Be aware that this html contains sensitive data whose presence on your server
417+
could trigger PCI DSS.
418+
419+
If your company is not certified PCI compliant, we recommend using
420+
`get_embed_url()` instead. You would then pass that returned URL to the
421+
frontend, where you can load it via an iframe.
422+
"""
423+
headers = {"Accept": "text/html", **(headers or {})}
424+
return await self._get(
425+
str(self.get_embed_url(query)),
426+
cast_to=str,
427+
options=make_request_options(headers, max_retries, timeout),
428+
)
429+
430+
def get_embed_url(self, query: CardGetEmbedURLParams) -> URL:
431+
"""
432+
Handling full card PANs and CVV codes requires that you comply with the Payment
433+
Card Industry Data Security Standards (PCI DSS). Some clients choose to reduce
434+
their compliance obligations by leveraging our embedded card UI solution
435+
documented below.
436+
437+
In this setup, PANs and CVV codes are presented to the end-user via a card UI
438+
that we provide, optionally styled in the customer's branding using a specified
439+
css stylesheet. A user's browser makes the request directly to api.lithic.com,
440+
so card PANs and CVVs never touch the API customer's servers while full card
441+
data is displayed to their end-users. The response contains an HTML document.
442+
This means that the url for the request can be inserted straight into the `src`
443+
attribute of an iframe.
444+
445+
```html
446+
<iframe
447+
id="card-iframe"
448+
src="https://sandbox.lithic.com/v1/embed/card?embed_request=eyJjc3MiO...;hmac=r8tx1..."
449+
allow="clipboard-write"
450+
class="content"
451+
></iframe>
452+
```
453+
454+
You should compute the request payload on the server side. You can render it (or
455+
the whole iframe) on the server or make an ajax call from your front end code,
456+
but **do not ever embed your API key into front end code, as doing so introduces
457+
a serious security vulnerability**.
458+
"""
459+
# Default expiration of 1 minute from now.
460+
query.setdefault("expiration", (datetime.now(timezone.utc) + timedelta(minutes=1)).isoformat())
461+
serialized = json.dumps(query, sort_keys=True, separators=(",", ":"))
462+
params = {
463+
"embed_request": base64.b64encode(bytes(serialized, "utf-8")).decode("utf-8"),
464+
"hmac": base64.b64encode(
465+
hmac.new(
466+
key=bytes(self._client.api_key, "utf-8"),
467+
msg=bytes(serialized, "utf-8"),
468+
digestmod=hashlib.sha256,
469+
).digest()
470+
).decode("utf-8"),
471+
}
472+
473+
# Copied nearly directly from httpx.BaseClient._merge_url
474+
base_url = self._client.base_url
475+
raw_path = base_url.raw_path + URL("embed/card").raw_path
476+
return base_url.copy_with(raw_path=raw_path).copy_merge_params(params)
477+
320478
async def provision(
321479
self,
322480
card_token: str,

lithic/types/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from .card_update_params import CardUpdateParams as CardUpdateParams
1414
from .account_list_params import AccountListParams as AccountListParams
1515
from .card_reissue_params import CardReissueParams as CardReissueParams
16+
from .embed_request_param import EmbedRequestParams as EmbedRequestParams
1617
from .account_update_params import AccountUpdateParams as AccountUpdateParams
1718
from .auth_rule_list_params import AuthRuleListParams as AuthRuleListParams
1819
from .card_provision_params import CardProvisionParams as CardProvisionParams
@@ -29,6 +30,8 @@
2930
from .auth_rule_create_response import AuthRuleCreateResponse as AuthRuleCreateResponse
3031
from .auth_rule_remove_response import AuthRuleRemoveResponse as AuthRuleRemoveResponse
3132
from .auth_rule_update_response import AuthRuleUpdateResponse as AuthRuleUpdateResponse
33+
from .card_get_embed_url_params import CardGetEmbedURLParams as CardGetEmbedURLParams
34+
from .card_get_embed_html_params import CardGetEmbedHTMLParams as CardGetEmbedHTMLParams
3235
from .funding_source_list_params import (
3336
FundingSourceListParams as FundingSourceListParams,
3437
)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# File generated from our OpenAPI spec by Stainless.
2+
3+
from __future__ import annotations
4+
5+
from typing_extensions import Required, TypedDict
6+
7+
__all__ = ["CardGetEmbedHTMLParams"]
8+
9+
10+
class CardGetEmbedHTMLParams(TypedDict, total=False):
11+
token: Required[str]
12+
"""Globally unique identifier for the card to be displayed."""
13+
14+
account_token: str
15+
"""Only needs to be included if one or more end-users have been enrolled."""
16+
17+
css: str
18+
"""
19+
A publicly available URI, so the white-labeled card element can be styled with
20+
the client's branding.
21+
"""
22+
23+
expiration: str
24+
"""An ISO 8601 timestamp for when the request should expire. UTC time zone.
25+
26+
If no timezone is specified, UTC will be used. If payload does not contain an
27+
expiration, the request will never expire.
28+
29+
Using an `expiration` reduces the risk of a
30+
[replay attack](https://en.wikipedia.org/wiki/Replay_attack). Without supplying
31+
the `expiration`, in the event that a malicious user gets a copy of your request
32+
in transit, they will be able to obtain the response data indefinitely.
33+
"""
34+
35+
target_origin: str
36+
"""Required if you want to post the element clicked to the parent iframe.
37+
38+
If you supply this param, you can also capture click events in the parent iframe
39+
by adding an event listener.
40+
"""
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# File generated from our OpenAPI spec by Stainless.
2+
3+
from __future__ import annotations
4+
5+
from typing_extensions import Required, TypedDict
6+
7+
__all__ = ["CardGetEmbedURLParams"]
8+
9+
10+
class CardGetEmbedURLParams(TypedDict, total=False):
11+
token: Required[str]
12+
"""Globally unique identifier for the card to be displayed."""
13+
14+
account_token: str
15+
"""Only needs to be included if one or more end-users have been enrolled."""
16+
17+
css: str
18+
"""
19+
A publicly available URI, so the white-labeled card element can be styled with
20+
the client's branding.
21+
"""
22+
23+
expiration: str
24+
"""An ISO 8601 timestamp for when the request should expire. UTC time zone.
25+
26+
If no timezone is specified, UTC will be used. If payload does not contain an
27+
expiration, the request will never expire.
28+
29+
Using an `expiration` reduces the risk of a
30+
[replay attack](https://en.wikipedia.org/wiki/Replay_attack). Without supplying
31+
the `expiration`, in the event that a malicious user gets a copy of your request
32+
in transit, they will be able to obtain the response data indefinitely.
33+
"""
34+
35+
target_origin: str
36+
"""Required if you want to post the element clicked to the parent iframe.
37+
38+
If you supply this param, you can also capture click events in the parent iframe
39+
by adding an event listener.
40+
"""
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# File generated from our OpenAPI spec by Stainless.
2+
3+
from typing import Optional
4+
5+
from .._models import BaseModel
6+
7+
__all__ = ["EmbedRequestParams"]
8+
9+
10+
class EmbedRequestParams(BaseModel):
11+
token: str
12+
"""Globally unique identifier for the card to be displayed."""
13+
14+
account_token: Optional[str]
15+
"""Only needs to be included if one or more end-users have been enrolled."""
16+
17+
css: Optional[str]
18+
"""
19+
A publicly available URI, so the white-labeled card element can be styled with
20+
the client's branding.
21+
"""
22+
23+
expiration: Optional[str]
24+
"""An ISO 8601 timestamp for when the request should expire. UTC time zone.
25+
26+
If no timezone is specified, UTC will be used. If payload does not contain an
27+
expiration, the request will never expire.
28+
29+
Using an `expiration` reduces the risk of a
30+
[replay attack](https://en.wikipedia.org/wiki/Replay_attack). Without supplying
31+
the `expiration`, in the event that a malicious user gets a copy of your request
32+
in transit, they will be able to obtain the response data indefinitely.
33+
"""
34+
35+
target_origin: Optional[str]
36+
"""Required if you want to post the element clicked to the parent iframe.
37+
38+
If you supply this param, you can also capture click events in the parent iframe
39+
by adding an event listener.
40+
"""

poetry.lock

Lines changed: 3 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ black = "^22.1.0"
2020
respx = "^0.19.2"
2121
pytest = "^7.1.1"
2222
pytest-asyncio = "^0.18.3"
23-
pyright = "^1.1.255"
23+
pyright = "^1.1.260"
2424
isort = "^5.10.1"
2525
autoflake = "^1.4"
2626

0 commit comments

Comments
 (0)