@tanqory/plugin-sdk 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -0
- package/dist/auth.d.ts +38 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +95 -0
- package/dist/auth.js.map +1 -0
- package/dist/client/index.d.ts +67 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +157 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client.d.ts +53 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +131 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +44 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.d.ts +54 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +117 -0
- package/dist/manifest.js.map +1 -0
- package/dist/manifest.spec.d.ts +2 -0
- package/dist/manifest.spec.d.ts.map +1 -0
- package/dist/manifest.spec.js +82 -0
- package/dist/manifest.spec.js.map +1 -0
- package/dist/webhooks.d.ts +22 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +74 -0
- package/dist/webhooks.js.map +1 -0
- package/dist/webhooks.spec.d.ts +2 -0
- package/dist/webhooks.spec.d.ts.map +1 -0
- package/dist/webhooks.spec.js +74 -0
- package/dist/webhooks.spec.js.map +1 -0
- package/package.json +67 -0
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# @tanqory/plugin-sdk
|
|
2
|
+
|
|
3
|
+
Build an app for the Tanqory marketplace. You host it; Tanqory frames it, routes
|
|
4
|
+
its API calls, and tells it who the merchant is.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install @tanqory/plugin-sdk
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## The two halves
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { createTanqoryApp, verifyWebhookSignature } from '@tanqory/plugin-sdk' // your server
|
|
14
|
+
import { useTanqoryApp } from '@tanqory/plugin-sdk/client' // your UI
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Separate entry points on purpose: importing the server half must not drag React
|
|
18
|
+
into a serverless function.
|
|
19
|
+
|
|
20
|
+
## Embedded UI
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
'use client'
|
|
24
|
+
import { useTanqoryApp } from '@tanqory/plugin-sdk/client'
|
|
25
|
+
|
|
26
|
+
export default function Page() {
|
|
27
|
+
const { ready, store, api } = useTanqoryApp()
|
|
28
|
+
if (!ready) return <p>Loading…</p>
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<button onClick={() => api!.get('/orders', { limit: 5 }).then(console.log)}>
|
|
32
|
+
Recent orders for {store!.storeName}
|
|
33
|
+
</button>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`useTanqoryApp` performs the whole handshake: verifying who framed you,
|
|
39
|
+
requesting store context, retrying an unanswered handshake, holding a token in
|
|
40
|
+
memory and renewing it before it expires, and reporting a dead session to the
|
|
41
|
+
host. Getting any of that wrong produces a blank frame with a clean console,
|
|
42
|
+
which is unpleasant to debug from the outside.
|
|
43
|
+
|
|
44
|
+
## Server side
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
const tanqory = createTanqoryApp({
|
|
48
|
+
clientId: process.env.TANQORY_CLIENT_ID!,
|
|
49
|
+
clientSecret: process.env.TANQORY_CLIENT_SECRET!,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const orders = await tanqory.store(storeId).get('/orders', { query: { limit: 5 } })
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Tokens are cached per store and renewals are single-flighted, so parallel work
|
|
56
|
+
triggers one exchange rather than one each.
|
|
57
|
+
|
|
58
|
+
## Webhooks
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
export async function POST(request: Request) {
|
|
62
|
+
const raw = await request.text()
|
|
63
|
+
if (!verifyWebhookSignature(raw, request.headers.get('x-tanqory-signature'), SECRET)) {
|
|
64
|
+
return new Response('bad signature', { status: 401 })
|
|
65
|
+
}
|
|
66
|
+
…
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Pass the **raw** body. Re-serialising a parsed object changes key order and
|
|
71
|
+
whitespace, and the signature will not match.
|
|
72
|
+
|
|
73
|
+
## Three things that will save you a day
|
|
74
|
+
|
|
75
|
+
**Never name an API host.** Every request is relative and Tanqory's edge resolves
|
|
76
|
+
which regional cell holds the store from the id in the path. An app that
|
|
77
|
+
hardcodes a hostname is pinned to one cell and breaks for merchants elsewhere.
|
|
78
|
+
|
|
79
|
+
**Never send `X-Frame-Options`.** It has no allow-list form, so `SAMEORIGIN`
|
|
80
|
+
overrides your `Content-Security-Policy: frame-ancestors` in browsers that still
|
|
81
|
+
read it, and the dashboard renders a blank frame. Set `frame-ancestors` instead.
|
|
82
|
+
|
|
83
|
+
**Never navigate to a login page.** You are inside an iframe: a login screen
|
|
84
|
+
renders in a box, and if it escaped it would throw the merchant out of their
|
|
85
|
+
dashboard. On auth failure the SDK tells the host and the host decides.
|
|
86
|
+
|
|
87
|
+
## The manifest
|
|
88
|
+
|
|
89
|
+
`tanqory.app.json` is what review approves, and the only thing a version pins.
|
|
90
|
+
Your code is yours — deploy whenever you like. Changing the _manifest_ (a new
|
|
91
|
+
scope, origin, or webhook topic) needs approval, because it changes what
|
|
92
|
+
merchants agreed to.
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"handle": "order-notifier",
|
|
97
|
+
"name": "Order Notifier",
|
|
98
|
+
"appUrl": "https://order-notifier.example.com",
|
|
99
|
+
"devUrl": "http://localhost:3000",
|
|
100
|
+
"embedded": { "enabled": true, "path": "/embed" },
|
|
101
|
+
"scopes": ["orders.view"],
|
|
102
|
+
"webhooks": [{ "topic": "orders/create", "path": "/hooks/orders" }]
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`devUrl` is used **instead of** `appUrl`, and only on a DEVELOPMENT store — so
|
|
107
|
+
you can point at localhost while building without any localhost origin ever
|
|
108
|
+
being trusted for a real merchant.
|
|
109
|
+
|
|
110
|
+
Validate it in your own build:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { parseAppManifest } from '@tanqory/plugin-sdk'
|
|
114
|
+
parseAppManifest(JSON.parse(readFileSync('tanqory.app.json', 'utf8')))
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
It reports every problem at once rather than stopping at the first.
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface TanqoryAppCredentials {
|
|
2
|
+
clientId: string;
|
|
3
|
+
clientSecret: string;
|
|
4
|
+
/** Defaults to `TANQORY_API_BASE`, then production. */
|
|
5
|
+
apiBase?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Exchanges your app credentials for a token scoped to one store.
|
|
9
|
+
*
|
|
10
|
+
* One instance per app; it caches per store and single-flights renewals, so N
|
|
11
|
+
* concurrent calls trigger at most one exchange rather than N. That matters
|
|
12
|
+
* because the token endpoint is rate limited, and a burst of parallel work is
|
|
13
|
+
* exactly when an app would otherwise hammer it.
|
|
14
|
+
*
|
|
15
|
+
* There is no refresh token by design. Renewal re-runs the exchange, which
|
|
16
|
+
* re-checks that the merchant still has your app installed — so uninstalling
|
|
17
|
+
* stops your access within one token lifetime rather than whenever a refresh
|
|
18
|
+
* token happened to expire.
|
|
19
|
+
*/
|
|
20
|
+
export declare class TanqoryAuth {
|
|
21
|
+
private readonly credentials;
|
|
22
|
+
private readonly apiBase;
|
|
23
|
+
private readonly cache;
|
|
24
|
+
private readonly inFlight;
|
|
25
|
+
constructor(credentials: TanqoryAppCredentials);
|
|
26
|
+
/** A token good right now for this store, renewing only when it has to. */
|
|
27
|
+
tokenFor(storeId: string): Promise<string>;
|
|
28
|
+
/** What the merchant actually granted. Useful for hiding UI you cannot serve. */
|
|
29
|
+
scopesFor(storeId: string): Promise<string[]>;
|
|
30
|
+
/**
|
|
31
|
+
* Drop a cached token — call after a 401 so the next request re-exchanges.
|
|
32
|
+
* Cheaper than pre-emptively shortening every lifetime.
|
|
33
|
+
*/
|
|
34
|
+
invalidate(storeId: string): void;
|
|
35
|
+
private entryFor;
|
|
36
|
+
private exchange;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAkBD;;;;;;;;;;;;GAYG;AACH,qBAAa,WAAW;IAKV,OAAO,CAAC,QAAQ,CAAC,WAAW;IAJxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiC;IACvD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA0C;gBAEtC,WAAW,EAAE,qBAAqB;IAQ/D,2EAA2E;IACrE,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIhD,iFAAiF;IAC3E,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAInD;;;OAGG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;YAInB,QAAQ;YAYR,QAAQ;CAsCvB"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TanqoryAuth = void 0;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
/** Where the platform lives. Overridable only for Tanqory's own dev tier. */
|
|
6
|
+
const DEFAULT_API_BASE = 'https://api.tanqory.com';
|
|
7
|
+
/**
|
|
8
|
+
* Renew this long before the token actually expires.
|
|
9
|
+
*
|
|
10
|
+
* A token that expires mid-flight produces a 401 the app cannot distinguish
|
|
11
|
+
* from a revoked installation, so it is worth spending a little lifetime to
|
|
12
|
+
* never see one.
|
|
13
|
+
*/
|
|
14
|
+
const EXPIRY_SKEW_MS = 30_000;
|
|
15
|
+
/**
|
|
16
|
+
* Exchanges your app credentials for a token scoped to one store.
|
|
17
|
+
*
|
|
18
|
+
* One instance per app; it caches per store and single-flights renewals, so N
|
|
19
|
+
* concurrent calls trigger at most one exchange rather than N. That matters
|
|
20
|
+
* because the token endpoint is rate limited, and a burst of parallel work is
|
|
21
|
+
* exactly when an app would otherwise hammer it.
|
|
22
|
+
*
|
|
23
|
+
* There is no refresh token by design. Renewal re-runs the exchange, which
|
|
24
|
+
* re-checks that the merchant still has your app installed — so uninstalling
|
|
25
|
+
* stops your access within one token lifetime rather than whenever a refresh
|
|
26
|
+
* token happened to expire.
|
|
27
|
+
*/
|
|
28
|
+
class TanqoryAuth {
|
|
29
|
+
credentials;
|
|
30
|
+
apiBase;
|
|
31
|
+
cache = new Map();
|
|
32
|
+
inFlight = new Map();
|
|
33
|
+
constructor(credentials) {
|
|
34
|
+
this.credentials = credentials;
|
|
35
|
+
this.apiBase = (credentials.apiBase ??
|
|
36
|
+
process.env.TANQORY_API_BASE ??
|
|
37
|
+
DEFAULT_API_BASE).replace(/\/+$/, '');
|
|
38
|
+
}
|
|
39
|
+
/** A token good right now for this store, renewing only when it has to. */
|
|
40
|
+
async tokenFor(storeId) {
|
|
41
|
+
return (await this.entryFor(storeId)).token;
|
|
42
|
+
}
|
|
43
|
+
/** What the merchant actually granted. Useful for hiding UI you cannot serve. */
|
|
44
|
+
async scopesFor(storeId) {
|
|
45
|
+
return (await this.entryFor(storeId)).scopes;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Drop a cached token — call after a 401 so the next request re-exchanges.
|
|
49
|
+
* Cheaper than pre-emptively shortening every lifetime.
|
|
50
|
+
*/
|
|
51
|
+
invalidate(storeId) {
|
|
52
|
+
this.cache.delete(storeId);
|
|
53
|
+
}
|
|
54
|
+
async entryFor(storeId) {
|
|
55
|
+
const cached = this.cache.get(storeId);
|
|
56
|
+
if (cached && Date.now() < cached.usableUntil)
|
|
57
|
+
return cached;
|
|
58
|
+
const pending = this.inFlight.get(storeId);
|
|
59
|
+
if (pending)
|
|
60
|
+
return pending;
|
|
61
|
+
const exchange = this.exchange(storeId).finally(() => this.inFlight.delete(storeId));
|
|
62
|
+
this.inFlight.set(storeId, exchange);
|
|
63
|
+
return exchange;
|
|
64
|
+
}
|
|
65
|
+
async exchange(storeId) {
|
|
66
|
+
const response = await fetch(`${this.apiBase}/api/v1/apps/token`, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
69
|
+
body: JSON.stringify({
|
|
70
|
+
grant_type: 'client_credentials',
|
|
71
|
+
client_id: this.credentials.clientId,
|
|
72
|
+
client_secret: this.credentials.clientSecret,
|
|
73
|
+
store_id: storeId,
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
if (response.status === 401)
|
|
77
|
+
throw new errors_1.TanqoryAuthError();
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
throw new errors_1.TanqoryError(`Token exchange failed with ${response.status}.`, response.status, 'TOKEN_EXCHANGE');
|
|
80
|
+
}
|
|
81
|
+
const body = (await response.json());
|
|
82
|
+
if (!body.access_token) {
|
|
83
|
+
throw new errors_1.TanqoryError('Token exchange returned no access_token.', 500, 'TOKEN_EXCHANGE');
|
|
84
|
+
}
|
|
85
|
+
const entry = {
|
|
86
|
+
token: body.access_token,
|
|
87
|
+
usableUntil: Date.now() + Math.max((body.expires_in ?? 600) * 1000 - EXPIRY_SKEW_MS, 0),
|
|
88
|
+
scopes: body.scope ? body.scope.split(' ').filter(Boolean) : [],
|
|
89
|
+
};
|
|
90
|
+
this.cache.set(storeId, entry);
|
|
91
|
+
return entry;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
exports.TanqoryAuth = TanqoryAuth;
|
|
95
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type BridgeContext } from '@tanqory/app-bridge';
|
|
2
|
+
/**
|
|
3
|
+
* The embedded half of the SDK.
|
|
4
|
+
*
|
|
5
|
+
* Everything here runs in the browser, inside the iframe the Tanqory dashboard
|
|
6
|
+
* renders your app in. Kept in its own entry point so importing the server half
|
|
7
|
+
* cannot drag React into a serverless function.
|
|
8
|
+
*/
|
|
9
|
+
export type TanqoryAppStatus = 'loading' | 'ready' | 'error';
|
|
10
|
+
export interface TanqoryAppState {
|
|
11
|
+
status: TanqoryAppStatus;
|
|
12
|
+
ready: boolean;
|
|
13
|
+
error: string | null;
|
|
14
|
+
/** Non-null once `ready`. */
|
|
15
|
+
store: BridgeContext | null;
|
|
16
|
+
/** Call the API as the merchant currently looking at your app. */
|
|
17
|
+
api: EmbeddedApi | null;
|
|
18
|
+
/** True when the merchant granted this scope. Fails closed while loading. */
|
|
19
|
+
can: (scope: string) => boolean;
|
|
20
|
+
/** Show the dashboard's own toast — one rendered inside a sized iframe is clipped. */
|
|
21
|
+
toast: (variant: 'success' | 'error' | 'info', title: string, description?: string) => void;
|
|
22
|
+
}
|
|
23
|
+
export interface EmbeddedApi {
|
|
24
|
+
get<T = unknown>(path: string, query?: Record<string, string | number | boolean>): Promise<T>;
|
|
25
|
+
post<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
26
|
+
patch<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
27
|
+
delete<T = unknown>(path: string): Promise<T>;
|
|
28
|
+
}
|
|
29
|
+
export interface UseTanqoryAppOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Origins permitted to embed you. Defaults to Tanqory's dashboard.
|
|
32
|
+
*
|
|
33
|
+
* The bridge refuses to post to, or accept from, anything not on this list,
|
|
34
|
+
* and never uses `'*'` — so a page that frames your app without permission
|
|
35
|
+
* receives no context, no token, and no store data.
|
|
36
|
+
*/
|
|
37
|
+
allowedHostOrigins?: string[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Boot an embedded Tanqory app.
|
|
41
|
+
*
|
|
42
|
+
* ```tsx
|
|
43
|
+
* const { ready, store, api } = useTanqoryApp()
|
|
44
|
+
* if (!ready) return <p>Loading…</p>
|
|
45
|
+
* return <p>Hello {store.storeName}</p>
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* This replaces the handshake every embedded app would otherwise reimplement:
|
|
49
|
+
* creating the bridge guest, verifying who framed you, requesting context,
|
|
50
|
+
* retrying an unanswered handshake, holding a token in memory and renewing it
|
|
51
|
+
* before it expires, and telling the host when the session dies. Getting any of
|
|
52
|
+
* those wrong produces a blank frame with a clean console, which is a genuinely
|
|
53
|
+
* hard thing to debug from the outside.
|
|
54
|
+
*
|
|
55
|
+
* ## Two rules the platform enforces, which this respects for you
|
|
56
|
+
*
|
|
57
|
+
* **Never navigate to a login page.** Inside an iframe that renders a login
|
|
58
|
+
* screen in a box, and if it escaped it would throw the merchant out of their
|
|
59
|
+
* dashboard. On auth failure the host is told and decides.
|
|
60
|
+
*
|
|
61
|
+
* **Never name an API host.** Requests are relative; Tanqory's edge resolves
|
|
62
|
+
* which regional cell holds this store from the store id in the path. An app
|
|
63
|
+
* that hardcodes a host is pinned to one cell and breaks for merchants
|
|
64
|
+
* elsewhere.
|
|
65
|
+
*/
|
|
66
|
+
export declare function useTanqoryApp(options?: UseTanqoryAppOptions): TanqoryAppState;
|
|
67
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,aAAa,EAAoB,MAAM,qBAAqB,CAAA;AAG7F;;;;;;GAMG;AAEH,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAA;AAE5D,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,gBAAgB,CAAA;IACxB,KAAK,EAAE,OAAO,CAAA;IACd,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,6BAA6B;IAC7B,KAAK,EAAE,aAAa,GAAG,IAAI,CAAA;IAC3B,kEAAkE;IAClE,GAAG,EAAE,WAAW,GAAG,IAAI,CAAA;IACvB,6EAA6E;IAC7E,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAA;IAC/B,sFAAsF;IACtF,KAAK,EAAE,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO,GAAG,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CAC5F;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;IAC7F,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;IAC3D,KAAK,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;IAC5D,MAAM,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;CAC9C;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;CAC9B;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,eAAe,CA4EjF"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useTanqoryApp = useTanqoryApp;
|
|
4
|
+
const app_bridge_1 = require("@tanqory/app-bridge");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const DEFAULT_HOST_ORIGINS = ['https://app.tanqory.com'];
|
|
7
|
+
/**
|
|
8
|
+
* Boot an embedded Tanqory app.
|
|
9
|
+
*
|
|
10
|
+
* ```tsx
|
|
11
|
+
* const { ready, store, api } = useTanqoryApp()
|
|
12
|
+
* if (!ready) return <p>Loading…</p>
|
|
13
|
+
* return <p>Hello {store.storeName}</p>
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* This replaces the handshake every embedded app would otherwise reimplement:
|
|
17
|
+
* creating the bridge guest, verifying who framed you, requesting context,
|
|
18
|
+
* retrying an unanswered handshake, holding a token in memory and renewing it
|
|
19
|
+
* before it expires, and telling the host when the session dies. Getting any of
|
|
20
|
+
* those wrong produces a blank frame with a clean console, which is a genuinely
|
|
21
|
+
* hard thing to debug from the outside.
|
|
22
|
+
*
|
|
23
|
+
* ## Two rules the platform enforces, which this respects for you
|
|
24
|
+
*
|
|
25
|
+
* **Never navigate to a login page.** Inside an iframe that renders a login
|
|
26
|
+
* screen in a box, and if it escaped it would throw the merchant out of their
|
|
27
|
+
* dashboard. On auth failure the host is told and decides.
|
|
28
|
+
*
|
|
29
|
+
* **Never name an API host.** Requests are relative; Tanqory's edge resolves
|
|
30
|
+
* which regional cell holds this store from the store id in the path. An app
|
|
31
|
+
* that hardcodes a host is pinned to one cell and breaks for merchants
|
|
32
|
+
* elsewhere.
|
|
33
|
+
*/
|
|
34
|
+
function useTanqoryApp(options = {}) {
|
|
35
|
+
const [status, setStatus] = (0, react_1.useState)('loading');
|
|
36
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
37
|
+
const [store, setStore] = (0, react_1.useState)(null);
|
|
38
|
+
const bridgeRef = (0, react_1.useRef)(null);
|
|
39
|
+
const tokenRef = (0, react_1.useRef)(null);
|
|
40
|
+
const booted = (0, react_1.useRef)(false);
|
|
41
|
+
(0, react_1.useEffect)(() => {
|
|
42
|
+
// React Strict Mode double-invokes effects in development. The handshake is
|
|
43
|
+
// not idempotent from the host's side, so guard it.
|
|
44
|
+
if (booted.current)
|
|
45
|
+
return;
|
|
46
|
+
booted.current = true;
|
|
47
|
+
const bridge = (0, app_bridge_1.createBridgeGuest)({
|
|
48
|
+
allowedOrigins: options.allowedHostOrigins ?? DEFAULT_HOST_ORIGINS,
|
|
49
|
+
});
|
|
50
|
+
bridgeRef.current = bridge;
|
|
51
|
+
if (!bridge.isEmbedded || !bridge.hostOrigin) {
|
|
52
|
+
setError('This app must be opened from the Tanqory dashboard.');
|
|
53
|
+
setStatus('error');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const stop = bridge.start();
|
|
57
|
+
let cancelled = false;
|
|
58
|
+
void (async () => {
|
|
59
|
+
try {
|
|
60
|
+
// Retried, because a single unanswered handshake is unrecoverable and
|
|
61
|
+
// looks exactly like a hung app: the frame stays blank, the console is
|
|
62
|
+
// clean, and every network request is green.
|
|
63
|
+
let context = null;
|
|
64
|
+
for (let attempt = 1; attempt <= 3 && !context; attempt += 1) {
|
|
65
|
+
try {
|
|
66
|
+
context = await bridge.init();
|
|
67
|
+
}
|
|
68
|
+
catch (initError) {
|
|
69
|
+
if (cancelled)
|
|
70
|
+
return;
|
|
71
|
+
if (attempt === 3)
|
|
72
|
+
throw initError;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (cancelled || !context)
|
|
76
|
+
return;
|
|
77
|
+
setStore(context);
|
|
78
|
+
setStatus('ready');
|
|
79
|
+
bridge.ready();
|
|
80
|
+
}
|
|
81
|
+
catch (bootError) {
|
|
82
|
+
if (cancelled)
|
|
83
|
+
return;
|
|
84
|
+
setError(bootError instanceof Error ? bootError.message : 'Failed to start.');
|
|
85
|
+
setStatus('error');
|
|
86
|
+
}
|
|
87
|
+
})();
|
|
88
|
+
return () => {
|
|
89
|
+
cancelled = true;
|
|
90
|
+
stop();
|
|
91
|
+
};
|
|
92
|
+
}, [options.allowedHostOrigins]);
|
|
93
|
+
const api = store ? buildApi(bridgeRef, tokenRef, store.storeId) : null;
|
|
94
|
+
return {
|
|
95
|
+
status,
|
|
96
|
+
ready: status === 'ready',
|
|
97
|
+
error,
|
|
98
|
+
store,
|
|
99
|
+
api,
|
|
100
|
+
can: (scope) =>
|
|
101
|
+
// `'*'` is the store-owner wildcard. Without this branch the one merchant
|
|
102
|
+
// guaranteed to be allowed everything would see no actions at all.
|
|
103
|
+
store?.permissions.includes('*') || store?.permissions.includes(scope) || false,
|
|
104
|
+
toast: (variant, title, description) => bridgeRef.current?.post('TOAST', { variant, title, description }),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Renew this long before expiry — a token that dies mid-flight becomes a 401. */
|
|
108
|
+
const EXPIRY_SKEW_MS = 30_000;
|
|
109
|
+
function buildApi(bridgeRef, tokenRef, storeId) {
|
|
110
|
+
async function token() {
|
|
111
|
+
const cached = tokenRef.current;
|
|
112
|
+
if (cached && Date.now() < cached.expiresAt - EXPIRY_SKEW_MS)
|
|
113
|
+
return cached.value;
|
|
114
|
+
const bridge = bridgeRef.current;
|
|
115
|
+
if (!bridge)
|
|
116
|
+
throw new Error('Bridge is not ready.');
|
|
117
|
+
// The host holds a live token and answers instantly. It is also the only
|
|
118
|
+
// party that can recover a dead session, which is why failure is reported
|
|
119
|
+
// to it rather than handled here.
|
|
120
|
+
const { token: value, expiresAt } = await bridge.requestToken();
|
|
121
|
+
tokenRef.current = { value, expiresAt };
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
async function request(method, path, body, query) {
|
|
125
|
+
// Relative on purpose — see the note on `useTanqoryApp`.
|
|
126
|
+
const url = new URL(`/api/v1/stores/${storeId}${path.startsWith('/') ? path : `/${path}`}`, window.location.origin);
|
|
127
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
128
|
+
url.searchParams.set(key, String(value));
|
|
129
|
+
}
|
|
130
|
+
const response = await fetch(url, {
|
|
131
|
+
method,
|
|
132
|
+
headers: {
|
|
133
|
+
authorization: `Bearer ${await token()}`,
|
|
134
|
+
accept: 'application/json',
|
|
135
|
+
...(body === undefined ? {} : { 'content-type': 'application/json' }),
|
|
136
|
+
},
|
|
137
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
138
|
+
});
|
|
139
|
+
if (response.status === 401) {
|
|
140
|
+
tokenRef.current = null;
|
|
141
|
+
bridgeRef.current?.post('AUTH_EXPIRED', undefined);
|
|
142
|
+
throw new Error('The session expired.');
|
|
143
|
+
}
|
|
144
|
+
if (!response.ok)
|
|
145
|
+
throw new Error(`Request failed with ${response.status}.`);
|
|
146
|
+
if (response.status === 204)
|
|
147
|
+
return undefined;
|
|
148
|
+
return (await response.json());
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
get: (path, query) => request('GET', path, undefined, query),
|
|
152
|
+
post: (path, body) => request('POST', path, body),
|
|
153
|
+
patch: (path, body) => request('PATCH', path, body),
|
|
154
|
+
delete: (path) => request('DELETE', path),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { TanqoryAuth, type TanqoryAppCredentials } from './auth';
|
|
2
|
+
export interface RequestOptions {
|
|
3
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
4
|
+
body?: unknown;
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A REST client for one store.
|
|
10
|
+
*
|
|
11
|
+
* Paths are relative to that store: `get('/orders')` reaches
|
|
12
|
+
* `/api/v1/stores/<storeId>/orders`. You never assemble a store path yourself,
|
|
13
|
+
* which is the point — a hand-built path is how an app ends up reading a store
|
|
14
|
+
* it was not installed on and getting a confusing 403 instead of a clear one.
|
|
15
|
+
*
|
|
16
|
+
* There is no `baseURL` option, and no way to name a host. Tanqory runs one
|
|
17
|
+
* store's data in one of several regional cells, and which cell is not
|
|
18
|
+
* something an app should know, cache, or get wrong: the platform edge resolves
|
|
19
|
+
* it per request from the store id already in the path.
|
|
20
|
+
*/
|
|
21
|
+
export declare class TanqoryStoreClient {
|
|
22
|
+
private readonly auth;
|
|
23
|
+
private readonly storeId;
|
|
24
|
+
private readonly apiBase;
|
|
25
|
+
constructor(auth: TanqoryAuth, storeId: string, apiBase: string);
|
|
26
|
+
get<T = unknown>(path: string, options?: RequestOptions): Promise<T>;
|
|
27
|
+
post<T = unknown>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
|
|
28
|
+
put<T = unknown>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
|
|
29
|
+
patch<T = unknown>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
|
|
30
|
+
delete<T = unknown>(path: string, options?: RequestOptions): Promise<T>;
|
|
31
|
+
/** What the merchant granted this app on this store. */
|
|
32
|
+
scopes(): Promise<string[]>;
|
|
33
|
+
private request;
|
|
34
|
+
private messageFrom;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Entry point for server-side work.
|
|
38
|
+
*
|
|
39
|
+
* ```ts
|
|
40
|
+
* const tanqory = createTanqoryApp({
|
|
41
|
+
* clientId: process.env.TANQORY_CLIENT_ID!,
|
|
42
|
+
* clientSecret: process.env.TANQORY_CLIENT_SECRET!,
|
|
43
|
+
* })
|
|
44
|
+
*
|
|
45
|
+
* const orders = await tanqory.store(storeId).get('/orders', { query: { limit: 5 } })
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function createTanqoryApp(credentials: TanqoryAppCredentials): {
|
|
49
|
+
/** A client bound to one store. Memoised, so calling it in a loop is free. */
|
|
50
|
+
store(storeId: string): TanqoryStoreClient;
|
|
51
|
+
auth: TanqoryAuth;
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,KAAK,qBAAqB,EAAE,MAAM,QAAQ,CAAA;AAGhE,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAA;IAC7D,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,kBAAkB;IAE3B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAFP,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM;IAGlC,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;IAIpE,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;IAIrF,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;IAIpF,KAAK,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;IAItF,MAAM,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;IAIvE,wDAAwD;IACxD,MAAM,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YAIb,OAAO;YAmDP,WAAW;CAQ1B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,qBAAqB;IAW/D,8EAA8E;mBAC/D,MAAM,GAAG,kBAAkB;;EAU7C"}
|