@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/dist/client.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TanqoryStoreClient = void 0;
|
|
4
|
+
exports.createTanqoryApp = createTanqoryApp;
|
|
5
|
+
const auth_1 = require("./auth");
|
|
6
|
+
const errors_1 = require("./errors");
|
|
7
|
+
/**
|
|
8
|
+
* A REST client for one store.
|
|
9
|
+
*
|
|
10
|
+
* Paths are relative to that store: `get('/orders')` reaches
|
|
11
|
+
* `/api/v1/stores/<storeId>/orders`. You never assemble a store path yourself,
|
|
12
|
+
* which is the point — a hand-built path is how an app ends up reading a store
|
|
13
|
+
* it was not installed on and getting a confusing 403 instead of a clear one.
|
|
14
|
+
*
|
|
15
|
+
* There is no `baseURL` option, and no way to name a host. Tanqory runs one
|
|
16
|
+
* store's data in one of several regional cells, and which cell is not
|
|
17
|
+
* something an app should know, cache, or get wrong: the platform edge resolves
|
|
18
|
+
* it per request from the store id already in the path.
|
|
19
|
+
*/
|
|
20
|
+
class TanqoryStoreClient {
|
|
21
|
+
auth;
|
|
22
|
+
storeId;
|
|
23
|
+
apiBase;
|
|
24
|
+
constructor(auth, storeId, apiBase) {
|
|
25
|
+
this.auth = auth;
|
|
26
|
+
this.storeId = storeId;
|
|
27
|
+
this.apiBase = apiBase;
|
|
28
|
+
}
|
|
29
|
+
get(path, options) {
|
|
30
|
+
return this.request('GET', path, options);
|
|
31
|
+
}
|
|
32
|
+
post(path, body, options) {
|
|
33
|
+
return this.request('POST', path, { ...options, body });
|
|
34
|
+
}
|
|
35
|
+
put(path, body, options) {
|
|
36
|
+
return this.request('PUT', path, { ...options, body });
|
|
37
|
+
}
|
|
38
|
+
patch(path, body, options) {
|
|
39
|
+
return this.request('PATCH', path, { ...options, body });
|
|
40
|
+
}
|
|
41
|
+
delete(path, options) {
|
|
42
|
+
return this.request('DELETE', path, options);
|
|
43
|
+
}
|
|
44
|
+
/** What the merchant granted this app on this store. */
|
|
45
|
+
scopes() {
|
|
46
|
+
return this.auth.scopesFor(this.storeId);
|
|
47
|
+
}
|
|
48
|
+
async request(method, path, options = {}, retriedAfter401 = false) {
|
|
49
|
+
const url = new URL(`${this.apiBase}/api/v1/stores/${this.storeId}${path.startsWith('/') ? path : `/${path}`}`);
|
|
50
|
+
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
51
|
+
if (value !== undefined)
|
|
52
|
+
url.searchParams.set(key, String(value));
|
|
53
|
+
}
|
|
54
|
+
const token = await this.auth.tokenFor(this.storeId);
|
|
55
|
+
const response = await fetch(url, {
|
|
56
|
+
method,
|
|
57
|
+
headers: {
|
|
58
|
+
authorization: `Bearer ${token}`,
|
|
59
|
+
accept: 'application/json',
|
|
60
|
+
...(options.body === undefined ? {} : { 'content-type': 'application/json' }),
|
|
61
|
+
...options.headers,
|
|
62
|
+
},
|
|
63
|
+
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
64
|
+
signal: options.signal,
|
|
65
|
+
});
|
|
66
|
+
// A 401 here usually means the token aged out between the cache check and
|
|
67
|
+
// the request landing. Drop it and try once more; a second 401 is a real
|
|
68
|
+
// rejection — most often the merchant uninstalled while we were working.
|
|
69
|
+
if (response.status === 401 && !retriedAfter401) {
|
|
70
|
+
this.auth.invalidate(this.storeId);
|
|
71
|
+
return this.request(method, path, options, true);
|
|
72
|
+
}
|
|
73
|
+
if (response.status === 401)
|
|
74
|
+
throw new errors_1.TanqoryAuthError();
|
|
75
|
+
// Deliberately its own type. A 403 from Tanqory means the merchant did not
|
|
76
|
+
// grant this scope, or apps cannot use that endpoint at all — retrying or
|
|
77
|
+
// re-authenticating will never fix it, and treating it as a generic failure
|
|
78
|
+
// sends apps into retry loops that look like abuse from our side.
|
|
79
|
+
if (response.status === 403) {
|
|
80
|
+
throw new errors_1.TanqoryScopeError(await this.messageFrom(response));
|
|
81
|
+
}
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
throw new errors_1.TanqoryError(await this.messageFrom(response), response.status);
|
|
84
|
+
}
|
|
85
|
+
if (response.status === 204)
|
|
86
|
+
return undefined;
|
|
87
|
+
return (await response.json());
|
|
88
|
+
}
|
|
89
|
+
async messageFrom(response) {
|
|
90
|
+
try {
|
|
91
|
+
const body = (await response.json());
|
|
92
|
+
return body.message ?? body.error ?? `Request failed with ${response.status}.`;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return `Request failed with ${response.status}.`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
exports.TanqoryStoreClient = TanqoryStoreClient;
|
|
100
|
+
/**
|
|
101
|
+
* Entry point for server-side work.
|
|
102
|
+
*
|
|
103
|
+
* ```ts
|
|
104
|
+
* const tanqory = createTanqoryApp({
|
|
105
|
+
* clientId: process.env.TANQORY_CLIENT_ID!,
|
|
106
|
+
* clientSecret: process.env.TANQORY_CLIENT_SECRET!,
|
|
107
|
+
* })
|
|
108
|
+
*
|
|
109
|
+
* const orders = await tanqory.store(storeId).get('/orders', { query: { limit: 5 } })
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
function createTanqoryApp(credentials) {
|
|
113
|
+
const auth = new auth_1.TanqoryAuth(credentials);
|
|
114
|
+
const apiBase = (credentials.apiBase ??
|
|
115
|
+
process.env.TANQORY_API_BASE ??
|
|
116
|
+
'https://api.tanqory.com').replace(/\/+$/, '');
|
|
117
|
+
const clients = new Map();
|
|
118
|
+
return {
|
|
119
|
+
/** A client bound to one store. Memoised, so calling it in a loop is free. */
|
|
120
|
+
store(storeId) {
|
|
121
|
+
let client = clients.get(storeId);
|
|
122
|
+
if (!client) {
|
|
123
|
+
client = new TanqoryStoreClient(auth, storeId, apiBase);
|
|
124
|
+
clients.set(storeId, client);
|
|
125
|
+
}
|
|
126
|
+
return client;
|
|
127
|
+
},
|
|
128
|
+
auth,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Base for everything this SDK throws, so an app can catch one type. */
|
|
2
|
+
export declare class TanqoryError extends Error {
|
|
3
|
+
readonly status?: number | undefined;
|
|
4
|
+
readonly code?: string | undefined;
|
|
5
|
+
constructor(message: string, status?: number | undefined, code?: string | undefined);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The credentials were rejected, or this app is not installed on that store.
|
|
9
|
+
*
|
|
10
|
+
* Tanqory answers both with the same message on purpose — telling them apart
|
|
11
|
+
* would let anyone enumerate which apps exist and which stores installed them.
|
|
12
|
+
* So do not branch on the reason; there isn't one.
|
|
13
|
+
*/
|
|
14
|
+
export declare class TanqoryAuthError extends TanqoryError {
|
|
15
|
+
constructor(message?: string);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The merchant has not granted this app the scope the endpoint needs, or the
|
|
19
|
+
* endpoint is not available to apps at all.
|
|
20
|
+
*
|
|
21
|
+
* Retrying will not help. Ask for the scope in your next manifest version and
|
|
22
|
+
* the merchant will be prompted to re-approve.
|
|
23
|
+
*/
|
|
24
|
+
export declare class TanqoryScopeError extends TanqoryError {
|
|
25
|
+
constructor(message?: string);
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,qBAAa,YAAa,SAAQ,KAAK;IAGnC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM;gBAFtB,OAAO,EAAE,MAAM,EACN,MAAM,CAAC,EAAE,MAAM,YAAA,EACf,IAAI,CAAC,EAAE,MAAM,YAAA;CAKzB;AAED;;;;;;GAMG;AACH,qBAAa,gBAAiB,SAAQ,YAAY;gBACpC,OAAO,SAA4E;CAIhG;AAED;;;;;;GAMG;AACH,qBAAa,iBAAkB,SAAQ,YAAY;gBACrC,OAAO,SAAsD;CAI1E"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TanqoryScopeError = exports.TanqoryAuthError = exports.TanqoryError = void 0;
|
|
4
|
+
/** Base for everything this SDK throws, so an app can catch one type. */
|
|
5
|
+
class TanqoryError extends Error {
|
|
6
|
+
status;
|
|
7
|
+
code;
|
|
8
|
+
constructor(message, status, code) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.name = 'TanqoryError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.TanqoryError = TanqoryError;
|
|
16
|
+
/**
|
|
17
|
+
* The credentials were rejected, or this app is not installed on that store.
|
|
18
|
+
*
|
|
19
|
+
* Tanqory answers both with the same message on purpose — telling them apart
|
|
20
|
+
* would let anyone enumerate which apps exist and which stores installed them.
|
|
21
|
+
* So do not branch on the reason; there isn't one.
|
|
22
|
+
*/
|
|
23
|
+
class TanqoryAuthError extends TanqoryError {
|
|
24
|
+
constructor(message = 'Invalid client credentials, or this app is not installed on that store.') {
|
|
25
|
+
super(message, 401, 'AUTH');
|
|
26
|
+
this.name = 'TanqoryAuthError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.TanqoryAuthError = TanqoryAuthError;
|
|
30
|
+
/**
|
|
31
|
+
* The merchant has not granted this app the scope the endpoint needs, or the
|
|
32
|
+
* endpoint is not available to apps at all.
|
|
33
|
+
*
|
|
34
|
+
* Retrying will not help. Ask for the scope in your next manifest version and
|
|
35
|
+
* the merchant will be prompted to re-approve.
|
|
36
|
+
*/
|
|
37
|
+
class TanqoryScopeError extends TanqoryError {
|
|
38
|
+
constructor(message = 'This app is not permitted to perform that action.') {
|
|
39
|
+
super(message, 403, 'SCOPE');
|
|
40
|
+
this.name = 'TanqoryScopeError';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.TanqoryScopeError = TanqoryScopeError;
|
|
44
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":";;;AAAA,yEAAyE;AACzE,MAAa,YAAa,SAAQ,KAAK;IAG1B;IACA;IAHX,YACE,OAAe,EACN,MAAe,EACf,IAAa;QAEtB,KAAK,CAAC,OAAO,CAAC,CAAA;QAHL,WAAM,GAAN,MAAM,CAAS;QACf,SAAI,GAAJ,IAAI,CAAS;QAGtB,IAAI,CAAC,IAAI,GAAG,cAAc,CAAA;IAC5B,CAAC;CACF;AATD,oCASC;AAED;;;;;;GAMG;AACH,MAAa,gBAAiB,SAAQ,YAAY;IAChD,YAAY,OAAO,GAAG,yEAAyE;QAC7F,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,CAAA;QAC3B,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAA;IAChC,CAAC;CACF;AALD,4CAKC;AAED;;;;;;GAMG;AACH,MAAa,iBAAkB,SAAQ,YAAY;IACjD,YAAY,OAAO,GAAG,mDAAmD;QACvE,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,CAAA;QAC5B,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAA;IACjC,CAAC;CACF;AALD,8CAKC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a Tanqory marketplace app.
|
|
3
|
+
*
|
|
4
|
+
* This entry point is for your **server**: exchanging credentials, calling the
|
|
5
|
+
* API on a merchant's behalf, and verifying webhooks. The embedded UI half lives
|
|
6
|
+
* at `@tanqory/plugin-sdk/client`, kept separate so importing it here cannot
|
|
7
|
+
* drag React into a serverless function.
|
|
8
|
+
*/
|
|
9
|
+
export { TanqoryAuth, type TanqoryAppCredentials } from './auth';
|
|
10
|
+
export { createTanqoryApp, TanqoryStoreClient, type RequestOptions } from './client';
|
|
11
|
+
export { TanqoryError, TanqoryAuthError, TanqoryScopeError } from './errors';
|
|
12
|
+
export { verifyWebhookSignature, type VerifyOptions } from './webhooks';
|
|
13
|
+
export { appManifestSchema, ManifestError, parseAppManifest, type TanqoryAppManifest, } from './manifest';
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,WAAW,EAAE,KAAK,qBAAqB,EAAE,MAAM,QAAQ,CAAA;AAChE,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,KAAK,cAAc,EAAE,MAAM,UAAU,CAAA;AACpF,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAC5E,OAAO,EAAE,sBAAsB,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,CAAA;AACvE,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,gBAAgB,EAChB,KAAK,kBAAkB,GACxB,MAAM,YAAY,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Build a Tanqory marketplace app.
|
|
4
|
+
*
|
|
5
|
+
* This entry point is for your **server**: exchanging credentials, calling the
|
|
6
|
+
* API on a merchant's behalf, and verifying webhooks. The embedded UI half lives
|
|
7
|
+
* at `@tanqory/plugin-sdk/client`, kept separate so importing it here cannot
|
|
8
|
+
* drag React into a serverless function.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.parseAppManifest = exports.ManifestError = exports.appManifestSchema = exports.verifyWebhookSignature = exports.TanqoryScopeError = exports.TanqoryAuthError = exports.TanqoryError = exports.TanqoryStoreClient = exports.createTanqoryApp = exports.TanqoryAuth = void 0;
|
|
12
|
+
var auth_1 = require("./auth");
|
|
13
|
+
Object.defineProperty(exports, "TanqoryAuth", { enumerable: true, get: function () { return auth_1.TanqoryAuth; } });
|
|
14
|
+
var client_1 = require("./client");
|
|
15
|
+
Object.defineProperty(exports, "createTanqoryApp", { enumerable: true, get: function () { return client_1.createTanqoryApp; } });
|
|
16
|
+
Object.defineProperty(exports, "TanqoryStoreClient", { enumerable: true, get: function () { return client_1.TanqoryStoreClient; } });
|
|
17
|
+
var errors_1 = require("./errors");
|
|
18
|
+
Object.defineProperty(exports, "TanqoryError", { enumerable: true, get: function () { return errors_1.TanqoryError; } });
|
|
19
|
+
Object.defineProperty(exports, "TanqoryAuthError", { enumerable: true, get: function () { return errors_1.TanqoryAuthError; } });
|
|
20
|
+
Object.defineProperty(exports, "TanqoryScopeError", { enumerable: true, get: function () { return errors_1.TanqoryScopeError; } });
|
|
21
|
+
var webhooks_1 = require("./webhooks");
|
|
22
|
+
Object.defineProperty(exports, "verifyWebhookSignature", { enumerable: true, get: function () { return webhooks_1.verifyWebhookSignature; } });
|
|
23
|
+
var manifest_1 = require("./manifest");
|
|
24
|
+
Object.defineProperty(exports, "appManifestSchema", { enumerable: true, get: function () { return manifest_1.appManifestSchema; } });
|
|
25
|
+
Object.defineProperty(exports, "ManifestError", { enumerable: true, get: function () { return manifest_1.ManifestError; } });
|
|
26
|
+
Object.defineProperty(exports, "parseAppManifest", { enumerable: true, get: function () { return manifest_1.parseAppManifest; } });
|
|
27
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;AAEH,+BAAgE;AAAvD,mGAAA,WAAW,OAAA;AACpB,mCAAoF;AAA3E,0GAAA,gBAAgB,OAAA;AAAE,4GAAA,kBAAkB,OAAA;AAC7C,mCAA4E;AAAnE,sGAAA,YAAY,OAAA;AAAE,0GAAA,gBAAgB,OAAA;AAAE,2GAAA,iBAAiB,OAAA;AAC1D,uCAAuE;AAA9D,kHAAA,sBAAsB,OAAA;AAC/B,uCAKmB;AAJjB,6GAAA,iBAAiB,OAAA;AACjB,yGAAA,aAAa,OAAA;AACb,4GAAA,gBAAgB,OAAA"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tanqory.app.json` — the contract between an app and the platform.
|
|
3
|
+
*
|
|
4
|
+
* This file is what review approves, and it is the *only* thing a version pins.
|
|
5
|
+
* Your code is yours: you deploy whenever you like and Tanqory neither sees it
|
|
6
|
+
* nor needs to. Change the manifest — a new scope, a new origin, a new webhook
|
|
7
|
+
* topic — and that needs approving, because it changes what merchants agreed to.
|
|
8
|
+
*
|
|
9
|
+
* Validated by hand rather than with a schema library on purpose. This package
|
|
10
|
+
* is installed by strangers, and every dependency it carries is one they are
|
|
11
|
+
* being asked to trust; a hundred lines of checking is cheaper than that.
|
|
12
|
+
*/
|
|
13
|
+
export interface TanqoryAppManifest {
|
|
14
|
+
/** URL-safe identity. Immutable once published — it appears in install links. */
|
|
15
|
+
handle: string;
|
|
16
|
+
name: string;
|
|
17
|
+
/** Where Tanqory frames your app and sends merchants. */
|
|
18
|
+
appUrl: string;
|
|
19
|
+
/**
|
|
20
|
+
* Used **instead of** `appUrl`, and only when the installing store is a
|
|
21
|
+
* DEVELOPMENT store. This is what lets you point at `localhost` while you
|
|
22
|
+
* build without any localhost origin ever being trusted for a real merchant.
|
|
23
|
+
*/
|
|
24
|
+
devUrl?: string;
|
|
25
|
+
embedded?: {
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
path?: string;
|
|
28
|
+
};
|
|
29
|
+
/** Permission keys, from Tanqory's catalog. Each needs a justification at review. */
|
|
30
|
+
scopes: string[];
|
|
31
|
+
webhooks?: Array<{
|
|
32
|
+
topic: string;
|
|
33
|
+
path: string;
|
|
34
|
+
}>;
|
|
35
|
+
supportUrl?: string;
|
|
36
|
+
privacyPolicyUrl?: string;
|
|
37
|
+
}
|
|
38
|
+
export declare class ManifestError extends Error {
|
|
39
|
+
readonly problems: string[];
|
|
40
|
+
constructor(problems: string[]);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Parse and validate a manifest.
|
|
44
|
+
*
|
|
45
|
+
* Reports **every** problem at once rather than stopping at the first. A
|
|
46
|
+
* validator that surfaces one error per run turns a five-field mistake into five
|
|
47
|
+
* round trips, and this is the first thing a new developer runs.
|
|
48
|
+
*/
|
|
49
|
+
export declare function parseAppManifest(input: unknown): TanqoryAppManifest;
|
|
50
|
+
/** Kept for symmetry with tooling that expects a schema-shaped export. */
|
|
51
|
+
export declare const appManifestSchema: {
|
|
52
|
+
parse: typeof parseAppManifest;
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,kBAAkB;IACjC,iFAAiF;IACjF,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC9C,qFAAqF;IACrF,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,QAAQ,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACjD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,qBAAa,aAAc,SAAQ,KAAK;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE;gBAAlB,QAAQ,EAAE,MAAM,EAAE;CAIxC;AAoBD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,kBAAkB,CAmEnE;AAED,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB;;CAA8B,CAAA"}
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `tanqory.app.json` — the contract between an app and the platform.
|
|
4
|
+
*
|
|
5
|
+
* This file is what review approves, and it is the *only* thing a version pins.
|
|
6
|
+
* Your code is yours: you deploy whenever you like and Tanqory neither sees it
|
|
7
|
+
* nor needs to. Change the manifest — a new scope, a new origin, a new webhook
|
|
8
|
+
* topic — and that needs approving, because it changes what merchants agreed to.
|
|
9
|
+
*
|
|
10
|
+
* Validated by hand rather than with a schema library on purpose. This package
|
|
11
|
+
* is installed by strangers, and every dependency it carries is one they are
|
|
12
|
+
* being asked to trust; a hundred lines of checking is cheaper than that.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.appManifestSchema = exports.ManifestError = void 0;
|
|
16
|
+
exports.parseAppManifest = parseAppManifest;
|
|
17
|
+
class ManifestError extends Error {
|
|
18
|
+
problems;
|
|
19
|
+
constructor(problems) {
|
|
20
|
+
super(`tanqory.app.json is not valid:\n - ${problems.join('\n - ')}`);
|
|
21
|
+
this.problems = problems;
|
|
22
|
+
this.name = 'ManifestError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.ManifestError = ManifestError;
|
|
26
|
+
const HANDLE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
27
|
+
function isHttpsUrl(value, { allowLocalhost = false } = {}) {
|
|
28
|
+
if (typeof value !== 'string')
|
|
29
|
+
return false;
|
|
30
|
+
let url;
|
|
31
|
+
try {
|
|
32
|
+
url = new URL(value);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (url.protocol === 'https:')
|
|
38
|
+
return true;
|
|
39
|
+
// http is tolerated for `devUrl` only, and only on localhost — a merchant is
|
|
40
|
+
// never sent to it, and requiring TLS on a laptop is friction with no payoff.
|
|
41
|
+
return (allowLocalhost && url.protocol === 'http:' && /^(localhost|127\.0\.0\.1)$/.test(url.hostname));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Parse and validate a manifest.
|
|
45
|
+
*
|
|
46
|
+
* Reports **every** problem at once rather than stopping at the first. A
|
|
47
|
+
* validator that surfaces one error per run turns a five-field mistake into five
|
|
48
|
+
* round trips, and this is the first thing a new developer runs.
|
|
49
|
+
*/
|
|
50
|
+
function parseAppManifest(input) {
|
|
51
|
+
const problems = [];
|
|
52
|
+
const manifest = (input ?? {});
|
|
53
|
+
if (typeof manifest.handle !== 'string' || !HANDLE.test(manifest.handle)) {
|
|
54
|
+
problems.push('`handle` must be lowercase letters, digits and single hyphens (e.g. "order-notifier")');
|
|
55
|
+
}
|
|
56
|
+
if (typeof manifest.name !== 'string' || manifest.name.trim().length === 0) {
|
|
57
|
+
problems.push('`name` is required');
|
|
58
|
+
}
|
|
59
|
+
if (!isHttpsUrl(manifest.appUrl)) {
|
|
60
|
+
problems.push('`appUrl` must be an https URL');
|
|
61
|
+
}
|
|
62
|
+
if (manifest.devUrl !== undefined && !isHttpsUrl(manifest.devUrl, { allowLocalhost: true })) {
|
|
63
|
+
problems.push('`devUrl` must be an https URL, or http on localhost');
|
|
64
|
+
}
|
|
65
|
+
if (!Array.isArray(manifest.scopes) || manifest.scopes.some((s) => typeof s !== 'string')) {
|
|
66
|
+
problems.push('`scopes` must be an array of permission keys');
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
for (const scope of manifest.scopes) {
|
|
71
|
+
// A duplicate is always a mistake, and silently de-duplicating it means
|
|
72
|
+
// the consent screen and the manifest disagree about what was asked for.
|
|
73
|
+
if (seen.has(scope))
|
|
74
|
+
problems.push(`\`scopes\` lists "${scope}" more than once`);
|
|
75
|
+
seen.add(scope);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (manifest.embedded !== undefined) {
|
|
79
|
+
const embedded = manifest.embedded;
|
|
80
|
+
if (typeof embedded?.enabled !== 'boolean') {
|
|
81
|
+
problems.push('`embedded.enabled` must be a boolean');
|
|
82
|
+
}
|
|
83
|
+
if (embedded?.path !== undefined && typeof embedded.path !== 'string') {
|
|
84
|
+
problems.push('`embedded.path` must be a string');
|
|
85
|
+
}
|
|
86
|
+
else if (typeof embedded?.path === 'string' && !embedded.path.startsWith('/')) {
|
|
87
|
+
problems.push('`embedded.path` must start with "/"');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (manifest.webhooks !== undefined) {
|
|
91
|
+
if (!Array.isArray(manifest.webhooks)) {
|
|
92
|
+
problems.push('`webhooks` must be an array');
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
manifest.webhooks.forEach((hook, index) => {
|
|
96
|
+
const entry = hook;
|
|
97
|
+
if (typeof entry?.topic !== 'string' || entry.topic.length === 0) {
|
|
98
|
+
problems.push(`\`webhooks[${index}].topic\` is required`);
|
|
99
|
+
}
|
|
100
|
+
if (typeof entry?.path !== 'string' || !entry.path.startsWith('/')) {
|
|
101
|
+
problems.push(`\`webhooks[${index}].path\` must be a path starting with "/"`);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const field of ['supportUrl', 'privacyPolicyUrl']) {
|
|
107
|
+
if (manifest[field] !== undefined && !isHttpsUrl(manifest[field])) {
|
|
108
|
+
problems.push(`\`${field}\` must be an https URL`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (problems.length > 0)
|
|
112
|
+
throw new ManifestError(problems);
|
|
113
|
+
return manifest;
|
|
114
|
+
}
|
|
115
|
+
/** Kept for symmetry with tooling that expects a schema-shaped export. */
|
|
116
|
+
exports.appManifestSchema = { parse: parseAppManifest };
|
|
117
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.spec.d.ts","sourceRoot":"","sources":["../src/manifest.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const manifest_1 = require("./manifest");
|
|
5
|
+
const VALID = {
|
|
6
|
+
handle: 'order-notifier',
|
|
7
|
+
name: 'Order Notifier',
|
|
8
|
+
appUrl: 'https://order-notifier.example.com',
|
|
9
|
+
scopes: ['orders.view'],
|
|
10
|
+
};
|
|
11
|
+
/** Collect the problems without asserting on prose. */
|
|
12
|
+
function problemsOf(input) {
|
|
13
|
+
try {
|
|
14
|
+
(0, manifest_1.parseAppManifest)(input);
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
return error instanceof manifest_1.ManifestError ? error.problems : [String(error)];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
(0, vitest_1.describe)('parseAppManifest', () => {
|
|
22
|
+
(0, vitest_1.it)('accepts a minimal manifest', () => {
|
|
23
|
+
(0, vitest_1.expect)((0, manifest_1.parseAppManifest)(VALID)).toMatchObject({ handle: 'order-notifier' });
|
|
24
|
+
});
|
|
25
|
+
(0, vitest_1.it)('reports every problem at once, not just the first', () => {
|
|
26
|
+
// A validator that stops at the first error turns a five-field mistake into
|
|
27
|
+
// five round trips — and this is the first thing a new developer runs.
|
|
28
|
+
const problems = problemsOf({ handle: 'Not Valid', appUrl: 'ftp://x', scopes: 'nope' });
|
|
29
|
+
(0, vitest_1.expect)(problems.length).toBeGreaterThanOrEqual(4);
|
|
30
|
+
});
|
|
31
|
+
(0, vitest_1.describe)('handle', () => {
|
|
32
|
+
vitest_1.it.each([
|
|
33
|
+
['uppercase', 'OrderNotifier'],
|
|
34
|
+
['spaces', 'order notifier'],
|
|
35
|
+
['leading hyphen', '-order'],
|
|
36
|
+
['double hyphen', 'order--notifier'],
|
|
37
|
+
['empty', ''],
|
|
38
|
+
])('rejects %s', (_label, handle) => {
|
|
39
|
+
(0, vitest_1.expect)(problemsOf({ ...VALID, handle })).not.toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
(0, vitest_1.describe)('urls', () => {
|
|
43
|
+
(0, vitest_1.it)('requires https for appUrl', () => {
|
|
44
|
+
(0, vitest_1.expect)(problemsOf({ ...VALID, appUrl: 'http://example.com' })).not.toEqual([]);
|
|
45
|
+
});
|
|
46
|
+
(0, vitest_1.it)('allows http on localhost for devUrl only', () => {
|
|
47
|
+
// A merchant is never sent to devUrl, and demanding TLS on a laptop is
|
|
48
|
+
// friction with no payoff.
|
|
49
|
+
(0, vitest_1.expect)(problemsOf({ ...VALID, devUrl: 'http://localhost:3000' })).toEqual([]);
|
|
50
|
+
(0, vitest_1.expect)(problemsOf({ ...VALID, devUrl: 'http://evil.example.com' })).not.toEqual([]);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
(0, vitest_1.describe)('scopes', () => {
|
|
54
|
+
(0, vitest_1.it)('rejects a duplicate', () => {
|
|
55
|
+
// Silently de-duplicating would make the consent screen and the manifest
|
|
56
|
+
// disagree about what was asked for.
|
|
57
|
+
(0, vitest_1.expect)(problemsOf({ ...VALID, scopes: ['orders.view', 'orders.view'] })).not.toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
(0, vitest_1.it)('accepts an empty list', () => {
|
|
60
|
+
// An app that only renders UI needs no scopes, and forcing one would push
|
|
61
|
+
// developers to request something they do not use.
|
|
62
|
+
(0, vitest_1.expect)(problemsOf({ ...VALID, scopes: [] })).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
(0, vitest_1.describe)('webhooks', () => {
|
|
66
|
+
(0, vitest_1.it)('requires a topic and a rooted path', () => {
|
|
67
|
+
(0, vitest_1.expect)(problemsOf({ ...VALID, webhooks: [{ topic: '', path: 'no-slash' }] }).length).toBe(2);
|
|
68
|
+
});
|
|
69
|
+
(0, vitest_1.it)('accepts a well-formed subscription', () => {
|
|
70
|
+
(0, vitest_1.expect)(problemsOf({ ...VALID, webhooks: [{ topic: 'orders/create', path: '/hooks/orders' }] })).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
(0, vitest_1.it)('requires embedded.path to be rooted when present', () => {
|
|
74
|
+
(0, vitest_1.expect)(problemsOf({ ...VALID, embedded: { enabled: true, path: 'embed' } })).not.toEqual([]);
|
|
75
|
+
(0, vitest_1.expect)(problemsOf({ ...VALID, embedded: { enabled: true, path: '/embed' } })).toEqual([]);
|
|
76
|
+
});
|
|
77
|
+
(0, vitest_1.it)('does not throw on null or a non-object', () => {
|
|
78
|
+
(0, vitest_1.expect)(problemsOf(null)).not.toEqual([]);
|
|
79
|
+
(0, vitest_1.expect)(problemsOf('a string')).not.toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
//# sourceMappingURL=manifest.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.spec.js","sourceRoot":"","sources":["../src/manifest.spec.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface VerifyOptions {
|
|
2
|
+
/** Override only if your clocks are genuinely worse than five minutes out. */
|
|
3
|
+
toleranceSeconds?: number;
|
|
4
|
+
/** Injectable for tests. */
|
|
5
|
+
now?: () => number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Verify that a webhook really came from Tanqory and has not been replayed.
|
|
9
|
+
*
|
|
10
|
+
* Pass the **raw** request body — the exact bytes received. Re-serialising a
|
|
11
|
+
* parsed object changes key order and whitespace, and the signature will not
|
|
12
|
+
* match. In frameworks that parse JSON for you, read the raw body first.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* const raw = await request.text()
|
|
16
|
+
* if (!verifyWebhookSignature(raw, request.headers.get('x-tanqory-signature'), secret)) {
|
|
17
|
+
* return new Response('bad signature', { status: 401 })
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function verifyWebhookSignature(rawBody: string, signatureHeader: string | null | undefined, secret: string, options?: VerifyOptions): boolean;
|
|
22
|
+
//# sourceMappingURL=webhooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,aAAa;IAC5B,8EAA8E;IAC9E,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,4BAA4B;IAC5B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC1C,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAiBT"}
|