@tanqory/plugin-sdk 0.5.0 → 0.6.1

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.
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The JSON Schema and `parseAppManifest` must agree.
3
+ *
4
+ * `parseAppManifest` is hand-written on purpose — this package is installed by
5
+ * strangers, and a schema library would be one more dependency they are asked to
6
+ * trust. So the schema shipped for editors and agents cannot be derived from it;
7
+ * it is written alongside, and would drift the moment anyone changed one and not
8
+ * the other.
9
+ *
10
+ * This is what stops that. Every case runs through both, and the two must reach
11
+ * the same verdict. A rule added to one and forgotten in the other fails here
12
+ * rather than in someone's editor six weeks later, telling them a manifest is
13
+ * fine that submission will reject.
14
+ *
15
+ * `ajv` is a devDependency. Nothing here ships.
16
+ */
17
+ export {};
18
+ //# sourceMappingURL=manifest.schema.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.schema.spec.d.ts","sourceRoot":"","sources":["../src/manifest.schema.spec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG"}
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ /**
3
+ * The JSON Schema and `parseAppManifest` must agree.
4
+ *
5
+ * `parseAppManifest` is hand-written on purpose — this package is installed by
6
+ * strangers, and a schema library would be one more dependency they are asked to
7
+ * trust. So the schema shipped for editors and agents cannot be derived from it;
8
+ * it is written alongside, and would drift the moment anyone changed one and not
9
+ * the other.
10
+ *
11
+ * This is what stops that. Every case runs through both, and the two must reach
12
+ * the same verdict. A rule added to one and forgotten in the other fails here
13
+ * rather than in someone's editor six weeks later, telling them a manifest is
14
+ * fine that submission will reject.
15
+ *
16
+ * `ajv` is a devDependency. Nothing here ships.
17
+ */
18
+ var __importDefault = (this && this.__importDefault) || function (mod) {
19
+ return (mod && mod.__esModule) ? mod : { "default": mod };
20
+ };
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ const node_fs_1 = require("node:fs");
23
+ const node_path_1 = require("node:path");
24
+ const ajv_1 = __importDefault(require("ajv"));
25
+ const vitest_1 = require("vitest");
26
+ const manifest_1 = require("./manifest");
27
+ const schema = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, '..', 'schema', 'tanqory-app.schema.json'), 'utf8'));
28
+ const ajv = new ajv_1.default({ allErrors: true, strict: false });
29
+ const validateWithSchema = ajv.compile(schema);
30
+ const VALID = {
31
+ handle: 'order-notifier',
32
+ name: 'Order Notifier',
33
+ appUrl: 'https://order-notifier.example.com',
34
+ devUrl: 'http://localhost:3000',
35
+ embedded: { enabled: true, path: '/embed' },
36
+ scopes: ['orders.view'],
37
+ webhooks: [{ topic: 'orders/create', path: '/hooks/orders' }],
38
+ };
39
+ /** What each side says about the same input. */
40
+ function verdicts(manifest) {
41
+ let validator = true;
42
+ try {
43
+ (0, manifest_1.parseAppManifest)(manifest);
44
+ }
45
+ catch {
46
+ validator = false;
47
+ }
48
+ return { schema: validateWithSchema(manifest), validator };
49
+ }
50
+ const cases = [
51
+ ['the scaffold default', VALID, true],
52
+ ['no devUrl', { ...VALID, devUrl: undefined }, true],
53
+ [
54
+ 'a manifest with every optional field',
55
+ {
56
+ ...VALID,
57
+ supportUrl: 'https://example.com/support',
58
+ privacyPolicyUrl: 'https://example.com/privacy',
59
+ appType: 'embed',
60
+ navigation: { placement: 'orders', items: [{ label: 'Settings', path: '/embed/settings' }] },
61
+ iframe: { minHeight: 600, width: 'full' },
62
+ },
63
+ true,
64
+ ],
65
+ [
66
+ 'notification capability with a dispatchUrl',
67
+ {
68
+ ...VALID,
69
+ provides: ['notification-channel:sms'],
70
+ notifications: { dispatchUrl: 'https://example.com/dispatch' },
71
+ },
72
+ true,
73
+ ],
74
+ ['an uppercase handle', { ...VALID, handle: 'OrderNotifier' }, false],
75
+ ['a handle with spaces', { ...VALID, handle: 'order notifier' }, false],
76
+ ['a leading hyphen in the handle', { ...VALID, handle: '-order' }, false],
77
+ ['no handle at all', { ...VALID, handle: undefined }, false],
78
+ ['an empty name', { ...VALID, name: '' }, false],
79
+ ['a plain http appUrl', { ...VALID, appUrl: 'http://example.com' }, false],
80
+ [
81
+ 'a devUrl on plain http that is not localhost',
82
+ { ...VALID, devUrl: 'http://example.com' },
83
+ false,
84
+ ],
85
+ ['scopes as a string', { ...VALID, scopes: 'orders.view' }, false],
86
+ ['a duplicated scope', { ...VALID, scopes: ['orders.view', 'orders.view'] }, false],
87
+ [
88
+ 'an embedded.path with no leading slash',
89
+ { ...VALID, embedded: { enabled: true, path: 'embed' } },
90
+ false,
91
+ ],
92
+ ['embedded.enabled as a string', { ...VALID, embedded: { enabled: 'yes' } }, false],
93
+ [
94
+ 'a webhook path with no leading slash',
95
+ { ...VALID, webhooks: [{ topic: 'orders/create', path: 'hooks' }] },
96
+ false,
97
+ ],
98
+ ['a webhook with no topic', { ...VALID, webhooks: [{ path: '/hooks' }] }, false],
99
+ ['appType outside the enum', { ...VALID, appType: 'popup' }, false],
100
+ ['iframe.width outside the enum', { ...VALID, iframe: { width: 'wide' } }, false],
101
+ ['a negative iframe.minHeight', { ...VALID, iframe: { minHeight: -1 } }, false],
102
+ ['navigation with no placement', { ...VALID, navigation: { items: [] } }, false],
103
+ [
104
+ 'a navigation item with no label',
105
+ { ...VALID, navigation: { placement: 'orders', items: [{ path: '/embed' }] } },
106
+ false,
107
+ ],
108
+ // The field that never existed but shipped in an early first-party manifest.
109
+ // Rejected by name so the fix is obvious rather than silently ignored.
110
+ ['the legacy embeddedPath field', { ...VALID, embeddedPath: '/embed' }, false],
111
+ // Taking over a notification with nowhere for it to go.
112
+ [
113
+ 'a notification capability with no dispatchUrl',
114
+ { ...VALID, provides: ['notification-override:order_confirmation'] },
115
+ false,
116
+ ],
117
+ [
118
+ 'a dispatchUrl on localhost',
119
+ {
120
+ ...VALID,
121
+ provides: ['notification-channel:sms'],
122
+ notifications: { dispatchUrl: 'http://localhost:3000/dispatch' },
123
+ },
124
+ false,
125
+ ],
126
+ ];
127
+ (0, vitest_1.describe)('the JSON Schema agrees with parseAppManifest', () => {
128
+ vitest_1.it.each(cases)('%s', (_label, manifest, expected) => {
129
+ const { schema: bySchema, validator: byValidator } = verdicts(manifest);
130
+ // The assertion that matters is that they agree. Comparing both to the
131
+ // expectation separately would let a case pass while the two disagreed
132
+ // about why.
133
+ (0, vitest_1.expect)({ schema: bySchema, validator: byValidator }).toEqual({
134
+ schema: expected,
135
+ validator: expected,
136
+ });
137
+ });
138
+ (0, vitest_1.it)('lists every field the type declares, so a new one cannot be forgotten', () => {
139
+ // `additionalProperties: false` means a field added to the type but not to
140
+ // the schema is rejected by an editor while submission accepts it — the
141
+ // drift that is hardest to spot, because the tool is wrong and the platform
142
+ // is right.
143
+ const declared = Object.keys(schema.properties).sort();
144
+ (0, vitest_1.expect)(declared).toEqual([
145
+ '$schema',
146
+ 'appType',
147
+ 'appUrl',
148
+ 'devUrl',
149
+ 'embedded',
150
+ 'embeddedPath',
151
+ 'handle',
152
+ 'iframe',
153
+ 'name',
154
+ 'navigation',
155
+ 'notifications',
156
+ 'privacyPolicyUrl',
157
+ 'provides',
158
+ 'scopes',
159
+ 'supportUrl',
160
+ 'webhooks',
161
+ ].sort());
162
+ });
163
+ });
164
+ //# sourceMappingURL=manifest.schema.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.schema.spec.js","sourceRoot":"","sources":["../src/manifest.schema.spec.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanqory/plugin-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/tanqory/tanqory-platform-packages.git",
@@ -29,10 +29,12 @@
29
29
  "./client": {
30
30
  "types": "./dist/client/index.d.ts",
31
31
  "default": "./dist/client/index.js"
32
- }
32
+ },
33
+ "./schema/tanqory-app.schema.json": "./schema/tanqory-app.schema.json"
33
34
  },
34
35
  "files": [
35
36
  "dist",
37
+ "schema",
36
38
  "README.md"
37
39
  ],
38
40
  "scripts": {
@@ -56,6 +58,7 @@
56
58
  "devDependencies": {
57
59
  "@types/node": "^20.0.0",
58
60
  "@types/react": "^18.0.0",
61
+ "ajv": "^8.17.1",
59
62
  "react": "^18.0.0",
60
63
  "typescript": "^5.9.3",
61
64
  "vitest": "^3.2.6"
@@ -0,0 +1,192 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://unpkg.com/@tanqory/plugin-sdk/schema/tanqory-app.schema.json",
4
+ "title": "Tanqory app manifest",
5
+ "description": "tanqory.app.json \u2014 the contract a merchant consents to. Hand-maintained alongside parseAppManifest; manifest.schema.spec.ts asserts the two agree on every case.",
6
+ "type": "object",
7
+ "required": ["handle", "name", "appUrl", "scopes"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "$schema": {
11
+ "type": "string",
12
+ "description": "Editor hint only. Ignored by the platform."
13
+ },
14
+ "handle": {
15
+ "type": "string",
16
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
17
+ "description": "Immutable global identity, and part of every install link. Lowercase letters, digits and single hyphens."
18
+ },
19
+ "name": {
20
+ "type": "string",
21
+ "minLength": 1,
22
+ "description": "Display name. Editable, unlike the handle."
23
+ },
24
+ "appUrl": {
25
+ "type": "string",
26
+ "pattern": "^https://",
27
+ "description": "Where merchants are pointed. Must be https."
28
+ },
29
+ "devUrl": {
30
+ "type": "string",
31
+ "pattern": "^(https://|http://(localhost|127\\.0\\.0\\.1)(:[0-9]+)?(/|$))",
32
+ "description": "Framed instead of appUrl on a development-store test install. Plain http is allowed here, and only here, so localhost works."
33
+ },
34
+ "embedded": {
35
+ "type": "object",
36
+ "additionalProperties": false,
37
+ "required": ["enabled"],
38
+ "properties": {
39
+ "enabled": {
40
+ "type": "boolean"
41
+ },
42
+ "path": {
43
+ "type": "string",
44
+ "pattern": "^/",
45
+ "description": "Entry point the dashboard frames. The route beneath it must carry a [storeId] segment."
46
+ }
47
+ }
48
+ },
49
+ "embeddedPath": {
50
+ "not": {},
51
+ "description": "Not a manifest field. Use embedded: { enabled: true, path: \"...\" }. Declared here so an editor rejects it by name rather than ignoring it."
52
+ },
53
+ "scopes": {
54
+ "type": "array",
55
+ "uniqueItems": true,
56
+ "items": {
57
+ "type": "string",
58
+ "pattern": "^[a-z_]+\\.[a-z_]+$",
59
+ "description": "domain.action, from the closed catalogue at https://docs.tanqory.com/apps/scopes/. There is no read_* form."
60
+ },
61
+ "description": "What the install screen asks for. The token is capped to what was actually granted."
62
+ },
63
+ "webhooks": {
64
+ "type": "array",
65
+ "items": {
66
+ "type": "object",
67
+ "additionalProperties": false,
68
+ "required": ["topic", "path"],
69
+ "properties": {
70
+ "topic": {
71
+ "type": "string",
72
+ "minLength": 1
73
+ },
74
+ "path": {
75
+ "type": "string",
76
+ "pattern": "^/"
77
+ }
78
+ }
79
+ }
80
+ },
81
+ "supportUrl": {
82
+ "type": "string",
83
+ "pattern": "^https://"
84
+ },
85
+ "privacyPolicyUrl": {
86
+ "type": "string",
87
+ "pattern": "^https://"
88
+ },
89
+ "appType": {
90
+ "enum": ["embed", "external"],
91
+ "description": "external means open in a new tab, not iframe."
92
+ },
93
+ "navigation": {
94
+ "type": "object",
95
+ "additionalProperties": false,
96
+ "required": ["placement"],
97
+ "properties": {
98
+ "placement": {
99
+ "type": "string",
100
+ "description": "Which dashboard menu the app appears under, or `root` for its own top-level entry. Shape-checked here and resolved server-side against the live list, so the vocabulary can grow without an SDK release.",
101
+ "examples": [
102
+ "apps",
103
+ "orders",
104
+ "products",
105
+ "customers",
106
+ "marketing",
107
+ "content",
108
+ "analytics",
109
+ "discounts",
110
+ "store-automation",
111
+ "online-store",
112
+ "subscriptions",
113
+ "markets",
114
+ "root"
115
+ ]
116
+ },
117
+ "items": {
118
+ "type": "array",
119
+ "items": {
120
+ "type": "object",
121
+ "additionalProperties": false,
122
+ "required": ["label", "path"],
123
+ "properties": {
124
+ "label": {
125
+ "type": "string",
126
+ "minLength": 1
127
+ },
128
+ "path": {
129
+ "type": "string",
130
+ "pattern": "^/",
131
+ "description": "Resolves inside your embedded surface. Declaring it does not create the route."
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
137
+ },
138
+ "iframe": {
139
+ "type": "object",
140
+ "additionalProperties": false,
141
+ "properties": {
142
+ "minHeight": {
143
+ "type": "number",
144
+ "exclusiveMinimum": 0
145
+ },
146
+ "width": {
147
+ "enum": ["full", "centered"]
148
+ }
149
+ }
150
+ },
151
+ "provides": {
152
+ "type": "array",
153
+ "uniqueItems": true,
154
+ "items": {
155
+ "type": "string",
156
+ "description": "A bare capability slug, or a namespaced notification-override:<type> / notification-channel:<channel>. The family is checked here; the name is checked against a live registry at submission."
157
+ }
158
+ },
159
+ "notifications": {
160
+ "type": "object",
161
+ "additionalProperties": false,
162
+ "required": ["dispatchUrl"],
163
+ "properties": {
164
+ "dispatchUrl": {
165
+ "type": "string",
166
+ "pattern": "^https://",
167
+ "description": "Where Tanqory posts a message you took over. Must be reachable from the platform \u2014 not localhost, because Tanqory calls it, your browser does not."
168
+ }
169
+ }
170
+ }
171
+ },
172
+ "allOf": [
173
+ {
174
+ "$comment": "provides naming a notification capability requires notifications.dispatchUrl \u2014 otherwise the messages you took over have nowhere to go.",
175
+ "if": {
176
+ "properties": {
177
+ "provides": {
178
+ "type": "array",
179
+ "contains": {
180
+ "type": "string",
181
+ "pattern": "^notification-(override|channel):"
182
+ }
183
+ }
184
+ },
185
+ "required": ["provides"]
186
+ },
187
+ "then": {
188
+ "required": ["notifications"]
189
+ }
190
+ }
191
+ ]
192
+ }