@shopimind/integration-kit-js 1.0.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.
Files changed (73) hide show
  1. package/LICENSE +10 -0
  2. package/README.md +118 -0
  3. package/dist/config/config-store.d.ts +6 -0
  4. package/dist/config/config-store.js +56 -0
  5. package/dist/contracts/common.d.ts +11 -0
  6. package/dist/contracts/common.js +1 -0
  7. package/dist/contracts/config-schema.d.ts +79 -0
  8. package/dist/contracts/config-schema.js +1 -0
  9. package/dist/contracts/index.d.ts +11 -0
  10. package/dist/contracts/index.js +1 -0
  11. package/dist/contracts/lifecycle.d.ts +59 -0
  12. package/dist/contracts/lifecycle.js +1 -0
  13. package/dist/contracts/sdk.d.ts +68 -0
  14. package/dist/contracts/sdk.js +6 -0
  15. package/dist/contracts/widget.d.ts +70 -0
  16. package/dist/contracts/widget.js +1 -0
  17. package/dist/http/routes.d.ts +18 -0
  18. package/dist/http/routes.js +150 -0
  19. package/dist/http/server.d.ts +7 -0
  20. package/dist/http/server.js +19 -0
  21. package/dist/index.d.ts +36 -0
  22. package/dist/index.js +47 -0
  23. package/dist/integration/define-integration.d.ts +16 -0
  24. package/dist/integration/define-integration.js +50 -0
  25. package/dist/integration/types.d.ts +148 -0
  26. package/dist/integration/types.js +1 -0
  27. package/dist/lifecycle/dispatcher.d.ts +33 -0
  28. package/dist/lifecycle/dispatcher.js +315 -0
  29. package/dist/lifecycle/inbound.d.ts +50 -0
  30. package/dist/lifecycle/inbound.js +124 -0
  31. package/dist/logging/logger.d.ts +23 -0
  32. package/dist/logging/logger.js +23 -0
  33. package/dist/manifest.d.ts +52 -0
  34. package/dist/manifest.js +36 -0
  35. package/dist/provisioning/ensure.d.ts +24 -0
  36. package/dist/provisioning/ensure.js +104 -0
  37. package/dist/provisioning/runner.d.ts +16 -0
  38. package/dist/provisioning/runner.js +49 -0
  39. package/dist/runtime/create-app.d.ts +66 -0
  40. package/dist/runtime/create-app.js +211 -0
  41. package/dist/runtime/rate-limiter.d.ts +19 -0
  42. package/dist/runtime/rate-limiter.js +46 -0
  43. package/dist/sdk/send-bulk.d.ts +46 -0
  44. package/dist/sdk/send-bulk.js +40 -0
  45. package/dist/sdk/source-scope.d.ts +38 -0
  46. package/dist/sdk/source-scope.js +34 -0
  47. package/dist/security/crypto.d.ts +19 -0
  48. package/dist/security/crypto.js +82 -0
  49. package/dist/security/redaction.d.ts +15 -0
  50. package/dist/security/redaction.js +56 -0
  51. package/dist/security/signature.d.ts +31 -0
  52. package/dist/security/signature.js +30 -0
  53. package/dist/store/db.d.ts +7 -0
  54. package/dist/store/db.js +22 -0
  55. package/dist/store/migrate.d.ts +10 -0
  56. package/dist/store/migrate.js +35 -0
  57. package/dist/store/migrations.d.ts +27 -0
  58. package/dist/store/migrations.js +128 -0
  59. package/dist/store/repositories.d.ts +102 -0
  60. package/dist/store/repositories.js +281 -0
  61. package/dist/store/types.d.ts +62 -0
  62. package/dist/store/types.js +1 -0
  63. package/dist/sync/concurrency.d.ts +12 -0
  64. package/dist/sync/concurrency.js +30 -0
  65. package/dist/sync/cursor.d.ts +16 -0
  66. package/dist/sync/cursor.js +14 -0
  67. package/dist/sync/engine.d.ts +49 -0
  68. package/dist/sync/engine.js +129 -0
  69. package/dist/sync/paginate.d.ts +14 -0
  70. package/dist/sync/paginate.js +42 -0
  71. package/dist/testing/harness.d.ts +49 -0
  72. package/dist/testing/harness.js +110 -0
  73. package/package.json +51 -0
@@ -0,0 +1,150 @@
1
+ import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
2
+ import { handleWebhook, handleTestConnection, handleRemoteData, } from '../lifecycle/dispatcher.js';
3
+ import { handleInbound } from '../lifecycle/inbound.js';
4
+ import { verifyShopimindSignature } from '../security/signature.js';
5
+ /** Ephemeral (per-process) HMAC key to compare the admin token at fixed length. */
6
+ const ADMIN_CMP_KEY = randomBytes(32);
7
+ /** Constant-time comparison WITHOUT length leakage (compares HMAC digests). */
8
+ function constantTimeEqual(a, b) {
9
+ const da = createHmac('sha256', ADMIN_CMP_KEY).update(a).digest();
10
+ const db = createHmac('sha256', ADMIN_CMP_KEY).update(b).digest();
11
+ return timingSafeEqual(da, db);
12
+ }
13
+ const webhookPayload = {
14
+ parse: false,
15
+ output: 'data',
16
+ allow: 'application/json',
17
+ maxBytes: 1024 * 1024,
18
+ };
19
+ const smallPayload = {
20
+ parse: false,
21
+ output: 'data',
22
+ allow: 'application/json',
23
+ maxBytes: 64 * 1024,
24
+ };
25
+ const rawBody = (req) => {
26
+ const p = req.payload;
27
+ if (p == null)
28
+ return '';
29
+ return Buffer.isBuffer(p) ? p.toString('utf8') : String(p);
30
+ };
31
+ function sigOptsOf(d) {
32
+ const o = { secret: d.secret };
33
+ if (d.toleranceSeconds != null)
34
+ o.toleranceSeconds = d.toleranceSeconds;
35
+ if (d.now)
36
+ o.now = d.now;
37
+ return o;
38
+ }
39
+ function parseConfigs(body) {
40
+ try {
41
+ return body ? JSON.parse(body) : {};
42
+ }
43
+ catch {
44
+ return {};
45
+ }
46
+ }
47
+ function adminOk(req, token) {
48
+ if (!token)
49
+ return false;
50
+ const x = req.headers['x-admin-token'];
51
+ const auth = req.headers.authorization;
52
+ const presented = typeof x === 'string' && x
53
+ ? x
54
+ : typeof auth === 'string'
55
+ ? auth.replace(/^Bearer\s+/i, '')
56
+ : '';
57
+ if (!presented)
58
+ return false;
59
+ return constantTimeEqual(presented, token);
60
+ }
61
+ const clientIp = (req) => req.info?.remoteAddress || 'unknown';
62
+ export function buildRoutes(deps) {
63
+ return [
64
+ {
65
+ method: 'POST',
66
+ path: '/webhook/receive',
67
+ options: { payload: webhookPayload },
68
+ handler: async (req, h) => {
69
+ // Per-IP rate limit BEFORE HMAC verification: caps the cost of a flood of
70
+ // unsigned (or forged) requests, each of which would otherwise force an HMAC.
71
+ if (deps.webhookRateLimit && !deps.webhookRateLimit(clientIp(req)))
72
+ return h.response({ success: false, error: 'rate_limited' }).code(429);
73
+ const res = await handleWebhook(rawBody(req), req.headers, deps.dispatcher);
74
+ return h.response(res.body).code(res.status);
75
+ },
76
+ },
77
+ {
78
+ method: 'POST',
79
+ path: '/webhook/test-connection',
80
+ options: { payload: smallPayload },
81
+ handler: async (req, h) => {
82
+ const body = rawBody(req);
83
+ const sig = verifyShopimindSignature(body, req.headers, sigOptsOf(deps.dispatcher));
84
+ if (!sig.ok)
85
+ return h.response({ success: false, error: 'unauthorized' }).code(401);
86
+ return h.response(await handleTestConnection(parseConfigs(body), deps.dispatcher)).code(200);
87
+ },
88
+ },
89
+ {
90
+ method: 'POST',
91
+ path: '/webhook/remote-data/{resource}',
92
+ options: { payload: smallPayload },
93
+ handler: async (req, h) => {
94
+ const body = rawBody(req);
95
+ const sig = verifyShopimindSignature(body, req.headers, sigOptsOf(deps.dispatcher));
96
+ if (!sig.ok)
97
+ return h.response({ success: false, error: 'unauthorized' }).code(401);
98
+ const resource = String(req.params.resource);
99
+ return h.response(await handleRemoteData(resource, parseConfigs(body), deps.dispatcher)).code(200);
100
+ },
101
+ },
102
+ {
103
+ method: 'POST',
104
+ path: '/inbound/{action}',
105
+ options: { payload: smallPayload },
106
+ handler: async (req, h) => {
107
+ const action = String(req.params.action);
108
+ const res = await handleInbound(action, rawBody(req), req.headers, deps.inbound);
109
+ return h.response(res.body).code(res.status);
110
+ },
111
+ },
112
+ {
113
+ method: 'GET',
114
+ path: '/health',
115
+ handler: (_req, h) => h.response({ status: 'ok' }).code(200),
116
+ },
117
+ {
118
+ method: 'POST',
119
+ path: '/admin/sync/{id}',
120
+ options: { payload: { parse: false, maxBytes: 4 * 1024 } },
121
+ handler: async (req, h) => {
122
+ if (deps.adminRateLimit && !deps.adminRateLimit(clientIp(req)))
123
+ return h.response({ success: false, error: 'rate_limited' }).code(429);
124
+ if (!adminOk(req, deps.adminToken))
125
+ return h.response({ success: false, error: 'unauthorized' }).code(401);
126
+ const id = String(req.params.id ?? '');
127
+ if (!id)
128
+ return h.response({ success: false, error: 'invalid_id' }).code(400);
129
+ // `?full=true` forces a full backfill (initial re-sync); default = incremental.
130
+ const full = String(req.query?.full ?? '') === 'true';
131
+ const summary = await deps.runSyncForInstall(id, full);
132
+ return h.response({ success: true, summary }).code(200);
133
+ },
134
+ },
135
+ {
136
+ method: 'GET',
137
+ path: '/admin/status/{id}',
138
+ handler: (req, h) => {
139
+ if (deps.adminRateLimit && !deps.adminRateLimit(clientIp(req)))
140
+ return h.response({ success: false, error: 'rate_limited' }).code(429);
141
+ if (!adminOk(req, deps.adminToken))
142
+ return h.response({ success: false, error: 'unauthorized' }).code(401);
143
+ const id = String(req.params.id ?? '');
144
+ if (!id)
145
+ return h.response({ success: false, error: 'invalid_id' }).code(400);
146
+ return h.response({ runs: deps.recentRuns(id) }).code(200);
147
+ },
148
+ },
149
+ ];
150
+ }
@@ -0,0 +1,7 @@
1
+ import type { Server } from '@hapi/hapi';
2
+ export interface ServerOptions {
3
+ port: number;
4
+ host?: string;
5
+ }
6
+ /** Bare Hapi server (dual-stack by default, CORS off, validation logged). */
7
+ export declare function createServer(opts: ServerOptions): Server;
@@ -0,0 +1,19 @@
1
+ import Hapi from '@hapi/hapi';
2
+ /** Bare Hapi server (dual-stack by default, CORS off, validation logged). */
3
+ export function createServer(opts) {
4
+ return Hapi.server({
5
+ port: opts.port,
6
+ // INTENTIONAL: bind to 0.0.0.0 (all interfaces) by default. This server is meant to
7
+ // run as a containerized service where the orchestrator / ingress controls exposure;
8
+ // binding to a loopback-only address would make it unreachable from outside the pod.
9
+ host: opts.host ?? '0.0.0.0',
10
+ routes: {
11
+ cors: false,
12
+ // INTENTIONAL: failAction 'log' (do NOT reject). Routes parse the RAW request body
13
+ // themselves (HMAC is computed over the exact bytes, payload.parse=false), so Joi
14
+ // request validation is non-authoritative here -- making it blocking would risk
15
+ // rejecting otherwise-valid signed requests. We log validation findings instead.
16
+ validate: { failAction: 'log' },
17
+ },
18
+ });
19
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @shopimind/integration-kit-js — public surface.
3
+ *
4
+ * Primitives, persistence, authoring contract, sync engine and HTTP runtime
5
+ * (server, routes) to build a ShopiMind integration.
6
+ */
7
+ export * from './security/signature.js';
8
+ export * from './security/crypto.js';
9
+ export * from './security/redaction.js';
10
+ export * from './sync/paginate.js';
11
+ export * from './sync/concurrency.js';
12
+ export * from './sync/cursor.js';
13
+ export * from './sync/engine.js';
14
+ export * from '@shopimind/sdk-js';
15
+ export * from './sdk/source-scope.js';
16
+ export type { BulkResult, SendBulk, SendBulkOptions } from './sdk/send-bulk.js';
17
+ export * from './provisioning/ensure.js';
18
+ export * from './provisioning/runner.js';
19
+ export * from './config/config-store.js';
20
+ export * from './lifecycle/dispatcher.js';
21
+ export * from './lifecycle/inbound.js';
22
+ export * from './store/db.js';
23
+ export * from './store/migrate.js';
24
+ export * from './store/migrations.js';
25
+ export * from './store/repositories.js';
26
+ export type * from './store/types.js';
27
+ export * from './logging/logger.js';
28
+ export * from './integration/define-integration.js';
29
+ export type * from './integration/types.js';
30
+ export * from './manifest.js';
31
+ export * from './http/server.js';
32
+ export * from './http/routes.js';
33
+ export * from './runtime/create-app.js';
34
+ export * from './runtime/rate-limiter.js';
35
+ export * from './testing/harness.js';
36
+ export type * from './contracts/index.js';
package/dist/index.js ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @shopimind/integration-kit-js — public surface.
3
+ *
4
+ * Primitives, persistence, authoring contract, sync engine and HTTP runtime
5
+ * (server, routes) to build a ShopiMind integration.
6
+ */
7
+ // Security
8
+ export * from './security/signature.js';
9
+ export * from './security/crypto.js';
10
+ export * from './security/redaction.js';
11
+ // Sync (safe cursor)
12
+ export * from './sync/paginate.js';
13
+ export * from './sync/concurrency.js';
14
+ export * from './sync/cursor.js';
15
+ export * from './sync/engine.js';
16
+ // Re-export of the ShopiMind SDK: an integration imports everything from
17
+ // `@shopimind/integration-kit-js` (resources, SpmHelpers, types).
18
+ export * from '@shopimind/sdk-js';
19
+ // Provisioned source helper (`ctx.withSource` -> SourceHandle).
20
+ export * from './sdk/source-scope.js';
21
+ // Idempotent provisioning + plan runner
22
+ export * from './provisioning/ensure.js';
23
+ export * from './provisioning/runner.js';
24
+ // Persisted config (secrets encrypted at rest)
25
+ export * from './config/config-store.js';
26
+ // Lifecycle (dispatcher: signature + REDACTED log + provisioning)
27
+ export * from './lifecycle/dispatcher.js';
28
+ // Inbound routes (secured middleware: integrator app -> integration -> ShopiMind)
29
+ export * from './lifecycle/inbound.js';
30
+ // Persistence (versioned migrations + typed repositories)
31
+ export * from './store/db.js';
32
+ export * from './store/migrate.js';
33
+ export * from './store/migrations.js';
34
+ export * from './store/repositories.js';
35
+ // Redacting logger
36
+ export * from './logging/logger.js';
37
+ // Integration authoring contract
38
+ export * from './integration/define-integration.js';
39
+ // Neutral integration manifest (describes the integration in a portable way)
40
+ export * from './manifest.js';
41
+ // HTTP runtime (Hapi server + routes + bootstrap)
42
+ export * from './http/server.js';
43
+ export * from './http/routes.js';
44
+ export * from './runtime/create-app.js';
45
+ export * from './runtime/rate-limiter.js';
46
+ // Test helpers
47
+ export * from './testing/harness.js';
@@ -0,0 +1,16 @@
1
+ import type { Integration } from './types.js';
2
+ /**
3
+ * Entry point of the author contract. Identity + boot-time validation: a
4
+ * malformed spec throws early (rather than behaving silently wrong).
5
+ */
6
+ export declare function defineIntegration<S>(integration: Integration<S>): Integration<S>;
7
+ export declare function validateIntegration<S>(c: Integration<S>): void;
8
+ /**
9
+ * Validates the events of a materialized ProvisioningPlan: each event must carry
10
+ * a non-empty `code_name`. Called by the provisioning runner once the (async)
11
+ * plan has been produced — `validateIntegration` cannot do this because the plan
12
+ * depends on a runtime context.
13
+ */
14
+ export declare function validateProvisioningEvents(events: ReadonlyArray<{
15
+ code_name?: string;
16
+ }>): void;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Entry point of the author contract. Identity + boot-time validation: a
3
+ * malformed spec throws early (rather than behaving silently wrong).
4
+ */
5
+ export function defineIntegration(integration) {
6
+ validateIntegration(integration);
7
+ return integration;
8
+ }
9
+ export function validateIntegration(c) {
10
+ if (!c.slug || !/^[a-z0-9_-]+$/.test(c.slug)) {
11
+ throw new Error(`invalid integration.slug: "${c.slug}" (expected [a-z0-9_-]+)`);
12
+ }
13
+ // meta.version must be present and a strict semver-ish `x.y.z` (it feeds the manifest).
14
+ if (!c.meta || !/^\d+\.\d+\.\d+$/.test(c.meta.version ?? '')) {
15
+ throw new Error(`invalid integration.meta.version: "${c.meta?.version}" (expected x.y.z)`);
16
+ }
17
+ const entities = c.syncSteps.map((s) => s.entity);
18
+ // Each sync step must declare a non-empty entity (whitespace-only is rejected).
19
+ for (const step of c.syncSteps) {
20
+ if (typeof step.entity !== 'string' || step.entity.trim() === '') {
21
+ throw new Error('sync step has an empty entity');
22
+ }
23
+ }
24
+ const dup = entities.find((e, i) => entities.indexOf(e) !== i);
25
+ if (dup) {
26
+ throw new Error(`duplicate sync step for entity "${dup}"`);
27
+ }
28
+ for (const step of c.syncSteps) {
29
+ if (step.cursorScope === 'per-source' && !step.sources) {
30
+ throw new Error(`step "${step.entity}" has scope 'per-source' but does not declare sources()`);
31
+ }
32
+ }
33
+ // NOTE: provisioned events live in the ProvisioningPlan returned by the
34
+ // `provisioning(ctx)` function, which needs a runtime ctx and is therefore not
35
+ // reachable from this static, boot-time validator. Each event's `code_name` is
36
+ // validated by `validateProvisioningEvents` when the plan is materialized.
37
+ }
38
+ /**
39
+ * Validates the events of a materialized ProvisioningPlan: each event must carry
40
+ * a non-empty `code_name`. Called by the provisioning runner once the (async)
41
+ * plan has been produced — `validateIntegration` cannot do this because the plan
42
+ * depends on a runtime context.
43
+ */
44
+ export function validateProvisioningEvents(events) {
45
+ for (const ev of events) {
46
+ if (typeof ev.code_name !== 'string' || ev.code_name.trim() === '') {
47
+ throw new Error('provisioning event is missing a code_name');
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,148 @@
1
+ import type { ConfigSchema, RawConfigs, WidgetDeclaration, NewDataSource, NewCustomDataDefinition, NewEvent, SpmOrderStatus } from '../contracts/index.js';
2
+ import type { SpmHttpClient } from '@shopimind/sdk-js';
3
+ import type { SourceHandle } from '../sdk/source-scope.js';
4
+ import type { SendBulk } from '../sdk/send-bulk.js';
5
+ import type { IntegrationStateRepo } from '../store/repositories.js';
6
+ import type { CursorRow } from '../store/types.js';
7
+ import type { Logger } from '../logging/logger.js';
8
+ import type { PaginateOptions } from '../sync/paginate.js';
9
+ /**
10
+ * Integration metadata (feeds the manifest).
11
+ */
12
+ export interface IntegrationMeta {
13
+ name: string;
14
+ version: string;
15
+ categories?: string[];
16
+ icon_url?: string;
17
+ short_description?: string;
18
+ description?: string;
19
+ /** An integration may require external auth (OAuth). Defaults to false. */
20
+ requires_external_auth?: boolean;
21
+ documentation_url?: string;
22
+ }
23
+ export interface RemoteOption {
24
+ value: string;
25
+ label: string;
26
+ }
27
+ /** Context injected into an integration: typed settings, typed SDK, encrypted state, redacting logger. */
28
+ export interface IntegrationContext<S> {
29
+ /** OPAQUE installation token, issued by ShopiMind. Never interpret it. */
30
+ installationId: string;
31
+ settings: S;
32
+ /** Ready-to-use ShopiMind SDK client (`SpmCustomers.bulkSave(ctx.spm, ...)`). */
33
+ spm: SpmHttpClient;
34
+ /**
35
+ * Safe bulk push (envelope + rejection handling), usable here AND in sync steps.
36
+ * Throws on a transport failure; surfaces per-item rejections — never drops them.
37
+ * In a sync step, the rejections it reports HOLD the cursor (no silent data loss).
38
+ * `ctx.sendBulk(SpmCustomDataRecords.bulkSave, records)`
39
+ */
40
+ sendBulk: SendBulk;
41
+ state: IntegrationStateRepo;
42
+ logger: Logger;
43
+ /** Records the INTEGRATOR account tied to this installation (correlation bridge). */
44
+ setExternalAccount(account: {
45
+ id: string;
46
+ name?: string | null;
47
+ }): void;
48
+ /**
49
+ * PER-INSTALLATION HMAC secret for inbound routes (middleware). Pass it to the
50
+ * integrator's app (typically in `onActivate`) so it can sign its inbound calls.
51
+ * `''` in an ephemeral context (configuration assistant).
52
+ */
53
+ inboundSecret: string;
54
+ /**
55
+ * Handle SCOPED TO A PROVISIONED SOURCE: automatically tags each pushed catalog
56
+ * entity with its `id_data_source` (prevents overwriting native data).
57
+ * `sourceKey` must match a source declared in `provisioning.dataSources`.
58
+ * (Also namespace your identifiers: the source alone does not isolate them.)
59
+ */
60
+ withSource(sourceKey: string): SourceHandle;
61
+ }
62
+ export interface SyncWindow {
63
+ since: Date | null;
64
+ until: Date;
65
+ }
66
+ /** Context for a sync step: window derived from the cursor + bounded primitives. */
67
+ export interface SyncStepContext<S> extends IntegrationContext<S> {
68
+ entity: string;
69
+ /** '' for the global scope; a source id (e.g. store) for 'per-source'. */
70
+ sourceKey: string;
71
+ window: SyncWindow;
72
+ cursor: CursorRow | null;
73
+ /** Streaming pagination (avoids OOM). */
74
+ paginate<T>(fetchPage: (page: number) => Promise<T[]>, opts?: PaginateOptions): AsyncGenerator<T, void, void>;
75
+ /** Map with bounded concurrency (avoids 429s). */
76
+ mapConcurrent<I, O>(items: Iterable<I>, limit: number, fn: (item: I, index: number) => Promise<O>): Promise<O[]>;
77
+ }
78
+ export interface SyncStepResult {
79
+ items: number;
80
+ errors: string[];
81
+ /** Bound up to which the cursor should advance IF the run is clean. */
82
+ advanceCursorTo?: Date;
83
+ }
84
+ /**
85
+ * A synchronization step. The ENGINE manages the cursor (advances only if
86
+ * `errors` is empty) and the source iteration: the integration never touches
87
+ * the cursor.
88
+ */
89
+ export interface SyncStep<S> {
90
+ entity: string;
91
+ cursorScope: 'global' | 'per-source';
92
+ /**
93
+ * By default, per-item rejections reported during the step HOLD the cursor (the
94
+ * window is replayed next run) — no silent data loss. Set `true` for a windowed
95
+ * stream where a PERMANENT rejection (a malformed item the API always rejects)
96
+ * would otherwise freeze the window forever ("poison pill"). Even when `true`,
97
+ * rejections are still surfaced (logged + counted); only the cursor may advance.
98
+ */
99
+ tolerateRejects?: boolean;
100
+ enabled(settings: S): boolean;
101
+ /** For 'per-source': the source keys to iterate over (e.g. store ids). */
102
+ sources?(ctx: IntegrationContext<S>): Promise<string[]> | string[];
103
+ run(ctx: SyncStepContext<S>): Promise<SyncStepResult>;
104
+ }
105
+ /** ShopiMind resources to ensure on activation (find-or-create by the kit). */
106
+ export interface ProvisioningPlan {
107
+ /** `parentKey` references another source in the same plan (resolved to `parent_id`). */
108
+ dataSources?: Array<{
109
+ key: string;
110
+ decl: NewDataSource;
111
+ parentKey?: string;
112
+ }>;
113
+ customData?: NewCustomDataDefinition[];
114
+ events?: NewEvent[];
115
+ orderStatuses?: SpmOrderStatus[];
116
+ }
117
+ export interface LifecycleHooks<S> {
118
+ onInstall?(ctx: IntegrationContext<S>): Promise<void> | void;
119
+ onActivate?(ctx: IntegrationContext<S>): Promise<void> | void;
120
+ onDeactivate?(ctx: IntegrationContext<S>): Promise<void> | void;
121
+ onUninstall?(ctx: IntegrationContext<S>): Promise<void> | void;
122
+ onConfigUpdated?(ctx: IntegrationContext<S>): Promise<void> | void;
123
+ }
124
+ /**
125
+ * The author contract. An integration only writes pure functions + typed
126
+ * declarations, passed to `defineIntegration`.
127
+ */
128
+ export interface Integration<S> {
129
+ slug: string;
130
+ meta: IntegrationMeta;
131
+ configSchema: ConfigSchema;
132
+ parseSettings(raw: RawConfigs): S;
133
+ testConnection(ctx: IntegrationContext<S>): Promise<boolean>;
134
+ remoteData?: Record<string, (ctx: IntegrationContext<S>) => Promise<RemoteOption[]>>;
135
+ provisioning?(ctx: IntegrationContext<S>): ProvisioningPlan | Promise<ProvisioningPlan>;
136
+ widgets?: WidgetDeclaration[];
137
+ syncSteps: SyncStep<S>[];
138
+ hooks?: LifecycleHooks<S>;
139
+ /**
140
+ * INBOUND routes (the middleware): the integrator's app calls them to trigger
141
+ * an event / push data in REAL TIME. The kit authenticates (per-installation
142
+ * HMAC), resolves the installation and provides `ctx` (with `ctx.spm` ready).
143
+ * Each handler is GENERIC: it does whatever it wants through `ctx.spm` via the
144
+ * SDK (`SpmEvents.trigger(ctx.spm, ...)`, `SpmCustomDataRecords.bulkSave(ctx.spm, ...)`...).
145
+ * Exposed at `POST /inbound/{action}`.
146
+ */
147
+ inbound?: Record<string, (ctx: IntegrationContext<S>, payload: unknown) => Promise<void> | void>;
148
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ import type { RawConfigs, WebhookResponse, RemoteDataResponse } from '../contracts/index.js';
2
+ import type { Integration } from '../integration/types.js';
3
+ import { type SpmHttpClient } from '@shopimind/sdk-js';
4
+ import type { Repositories } from '../store/repositories.js';
5
+ import type { Logger } from '../logging/logger.js';
6
+ export declare const ACCESS_TOKEN_KEY = "__access_token";
7
+ /** State key where the provisioning result (sourceIds/defIds) is stored. */
8
+ export declare const PROVISIONING_KEY = "__provisioning";
9
+ export interface DispatcherDeps<S> {
10
+ integration: Integration<S>;
11
+ repos: Repositories;
12
+ secret: string;
13
+ toleranceSeconds?: number;
14
+ logger: Logger;
15
+ /** Builds the ShopiMind SDK client for an access_token. */
16
+ makeSpmClient(accessToken: string): SpmHttpClient;
17
+ /** Called after a successful activation (the runtime starts the backfill here). */
18
+ afterActivate?: (installationId: string) => void;
19
+ now?: () => number;
20
+ }
21
+ export interface HttpResult {
22
+ status: number;
23
+ body: WebhookResponse;
24
+ }
25
+ /**
26
+ * Entry point for lifecycle webhooks. Verifies the signature against the RAW
27
+ * body, logs a REDACTED payload (secrets never reach the log), then dispatches.
28
+ */
29
+ export declare function handleWebhook<S>(rawBody: string, headers: Record<string, string | string[] | undefined>, deps: DispatcherDeps<S>): Promise<HttpResult>;
30
+ /** Validates the integration credentials (wizard step). No ShopiMind access. */
31
+ export declare function handleTestConnection<S>(configs: RawConfigs, deps: DispatcherDeps<S>): Promise<WebhookResponse>;
32
+ /** Populates a dynamic select in the config wizard. */
33
+ export declare function handleRemoteData<S>(resource: string, configs: RawConfigs, deps: DispatcherDeps<S>): Promise<RemoteDataResponse>;