@mycreat-labs/integrations-sdk 0.0.0-stage → 0.1.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.
- package/CHANGELOG.md +14 -0
- package/README.md +54 -2
- package/dist/adapters/bullmq.cjs +110 -0
- package/dist/adapters/bullmq.cjs.map +1 -0
- package/dist/adapters/bullmq.d.cts +11 -0
- package/dist/adapters/bullmq.d.ts +11 -0
- package/dist/adapters/bullmq.js +85 -0
- package/dist/adapters/bullmq.js.map +1 -0
- package/dist/adapters/postgres.cjs +396 -0
- package/dist/adapters/postgres.cjs.map +1 -0
- package/dist/adapters/postgres.d.cts +51 -0
- package/dist/adapters/postgres.d.ts +51 -0
- package/dist/adapters/postgres.js +368 -0
- package/dist/adapters/postgres.js.map +1 -0
- package/dist/adapters/prisma.cjs +414 -0
- package/dist/adapters/prisma.cjs.map +1 -0
- package/dist/adapters/prisma.d.cts +15 -0
- package/dist/adapters/prisma.d.ts +15 -0
- package/dist/adapters/prisma.js +386 -0
- package/dist/adapters/prisma.js.map +1 -0
- package/dist/adapters/redis.cjs +125 -0
- package/dist/adapters/redis.cjs.map +1 -0
- package/dist/adapters/redis.d.cts +20 -0
- package/dist/adapters/redis.d.ts +20 -0
- package/dist/adapters/redis.js +98 -0
- package/dist/adapters/redis.js.map +1 -0
- package/dist/errors-3hDEtzuZ.d.cts +11 -0
- package/dist/errors-3hDEtzuZ.d.ts +11 -0
- package/dist/index.cjs +1328 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +128 -0
- package/dist/index.d.ts +128 -0
- package/dist/index.js +1297 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/index.cjs +1287 -0
- package/dist/providers/index.cjs.map +1 -0
- package/dist/providers/index.d.cts +144 -0
- package/dist/providers/index.d.ts +144 -0
- package/dist/providers/index.js +1252 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/testing.cjs +275 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +20 -0
- package/dist/testing.d.ts +20 -0
- package/dist/testing.js +248 -0
- package/dist/testing.js.map +1 -0
- package/dist/types-Y4-DoJ1A.d.cts +260 -0
- package/dist/types-Y4-DoJ1A.d.ts +260 -0
- package/docs/migration.md +23 -0
- package/docs/releases/0.1.0.md +46 -0
- package/docs/security.md +26 -0
- package/docs/verification.md +22 -0
- package/examples/embedded-host.ts +62 -0
- package/migrations/0001_initial.sql +40 -0
- package/package.json +91 -4
- package/schema.prisma +80 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.1 — 2026-11-01
|
|
4
|
+
|
|
5
|
+
- Accept Notion's documented `refresh_token: null` response without fabricating
|
|
6
|
+
a refresh credential or preserving a previously rotated refresh token. The
|
|
7
|
+
response field remains required; missing, empty or malformed values are rejected.
|
|
8
|
+
- Verify that non-refreshable grants remain usable without an advertised expiry,
|
|
9
|
+
require reauthorization on a definite 401 or advertised expiry, and never
|
|
10
|
+
automatically redispatch rejected or expired writes.
|
|
11
|
+
|
|
12
|
+
Contract: [Notion Create a token](https://developers.notion.com/reference/create-a-token).
|
|
13
|
+
This release requires no API or storage migration and does not modify the
|
|
14
|
+
published `0.1.0` artifact. Consumers choose when to upgrade their pinned version.
|
package/README.md
CHANGED
|
@@ -1,3 +1,55 @@
|
|
|
1
|
-
#
|
|
1
|
+
# App Integrations SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Product-embedded, server-side third-party identity and integration runtime. Each product owns its users, sessions, OAuth registrations, encryption keys, database, Redis and deployment. No shared service or cross-product account is required.
|
|
4
|
+
|
|
5
|
+
## Package and supported runtime
|
|
6
|
+
|
|
7
|
+
`@mycreat-labs/integrations-sdk` 0.1.0 targets Node.js 22.12+ and 24, with ESM, CommonJS and declaration exports. The main entry has no ORM, NestJS, Redis or BullMQ runtime dependency. Optional adapters are imported explicitly from `/prisma`, `/postgres`, `/redis` and `/bullmq`. `/providers` exports individually configured Google, Apple, GitHub, Notion and Raindrop factories. `/testing` is explicitly test-only.
|
|
8
|
+
|
|
9
|
+
The package is intended for an internal npm registry. Until a registry is configured and publication separately authorized, install the exact local `pnpm pack` artifact in each product's `vendor/` directory and commit its lockfile when committing the implementation. Do not depend on another checkout, a moving branch, or a workspace path across products. No package has been published by this implementation.
|
|
10
|
+
|
|
11
|
+
## Host integration
|
|
12
|
+
|
|
13
|
+
1. Copy the versioned models from `schema.prisma` into the product schema. Review and apply `migrations/0001_initial.sql` through the product migration pipeline. Existing integrations require the compatibility steps in [migration guidance](docs/migration.md). Importing the SDK never runs DDL.
|
|
14
|
+
2. Inject the product's client using `createPrismaStore(database)`, its provider registrations and an explicit `createTokenVault({ activeKeyId, keys })` keyring. Each key is a base64-encoded random 32-byte value; keep real keys out of source control.
|
|
15
|
+
3. Call `createIntegrationSdk({ namespace, store, vault, providers, hooks })`. Namespaces must distinguish product and environment. Missing providers are omitted; partially configured providers fail validation.
|
|
16
|
+
4. In the product's existing native transaction use `sdk.withStore(bindPrismaTransaction(tx))`. Enqueue the SDK operation and product records/outbox together. Neither the bridge nor SDK opens another connection in this path.
|
|
17
|
+
5. Mount the product's own authenticated routes and OAuth callbacks. Start the explicitly constructed runtime in the product's Worker. See [embedded-host example](examples/embedded-host.ts).
|
|
18
|
+
|
|
19
|
+
SDK authorization transactions last ten minutes, bind to a caller-generated cryptographically random browser binding, and are atomically consumed. Product routes must protect cookies, check CSRF/origin, restrict callback/return URLs, authenticate the current owner, apply product admission and rate policies, and finish the product's own account/session flow. Desktop/extensions keep their product authorization origins and handoff protocols. A Node SDK does not run inside Swift; native apps use official login components or the system browser and their own product backend.
|
|
20
|
+
|
|
21
|
+
`identity.start/complete` return a verified external identity only. `identity.startNative` issues a server-side nonce/state and `identity.verifyNative` consumes that challenge. An email is profile data, never sufficient proof for merging accounts. Provider identity namespaces are explicit and separate from platform-specific client IDs. Existing native Apple refresh/revocation and product session behavior must remain under their existing owner until deliberately migrated.
|
|
22
|
+
|
|
23
|
+
## Connections and operations
|
|
24
|
+
|
|
25
|
+
- `connections.start/complete/list/disconnect` manage local connections and independent consent. A fresh connection flow supersedes the previous unfinished connect flow for that same owner/provider/registration. Reauthorization checks account identity, authorization epoch and connection generation. Disconnect invalidates in-flight authorizations before scheduling remote cleanup.
|
|
26
|
+
- `resources.list` returns authorized external resources. Notion actions use `create_page` with typed `NotionCreatePageInput`; Raindrop actions use `create_bookmark` with typed `RaindropCreateBookmarkInput`. Input validation and factory types are exported from `/providers`; product IDs belong in `operations.enqueue(...hostContext)`, rather than provider content.
|
|
27
|
+
- `operations.enqueue/get/list/recover` use persistent idempotency and a request fingerprint. The SDK operation is the execution state source; products keep operation IDs and presentation projections. Successful content input is cleared, while status and remote receipts remain.
|
|
28
|
+
- Hooks receive the host's actual native transaction. `beforeExecute` checks local account/content/admission state immediately before persisting dispatch intent; `onResult` checks again before atomically committing successful SDK state, event and host projection. Keep hooks transactional and free of remote calls. Use a consistent lock order: SDK owner, product user, operation/connection as required; host transactions and callbacks must follow the same order.
|
|
29
|
+
- A remote write whose response was lost is reconciled from safe provider receipts. The SDK never promises exactly-once external writes. Without a trustworthy receipt, the operation stays `outcome_unknown` for inspection; it does not silently create another remote resource.
|
|
30
|
+
|
|
31
|
+
The v1 new-connection store deliberately allows one local owner per namespace/provider/registration/remote-account identity. It rejects cross-owner rebinding rather than pretending that separate token rows imply separate upstream grants. Products needing shared installations must supply a reviewed reference-counted grant adapter; Subtic's existing shared-grant lifecycle remains in its legacy store. Products still have separate OAuth registrations, databases and keys.
|
|
32
|
+
|
|
33
|
+
Notion supports upstream revocation. Raindrop does not document a supported revoke endpoint, so disconnect revokes the local connection and users can remove the application in Raindrop settings. A configured remote revocation is durable, blocks reconnect until settled, and can be retried with `operations.recover` even for a revoked connection. `eraseOwner` first persists an owner tombstone, removes content and regular work, and retains only outstanding revocation material; successful cleanup removes the remaining grant/connection records. Restore missing provider/key configuration before resolving retained cleanup work. Already-sent remote requests cannot be rolled back by a local account deletion.
|
|
34
|
+
|
|
35
|
+
## Worker and infrastructure
|
|
36
|
+
|
|
37
|
+
`createRuntime({ sdk, queue })` has explicit start/stop and health reporting. Use `createBullMqTransport({ connection, prefix, concurrency })` with product-owned Redis. Queue messages contain task ID/version; PostgreSQL is authoritative. Lost queue data, duplicate messages, crashed workers and expired leases are recovered from the database. The host should expose runtime health, monitor authorization/refresh failures and task age, and apply existing product notification/outbox policies.
|
|
38
|
+
|
|
39
|
+
`createRedisRateLimiter` provides shared admission to API and Worker requests. Inject it through `rateLimiter`; API/resource and credential-refresh budgets use separate keys so exhausted content requests cannot starve necessary refresh. Requests wait for admission for up to ten seconds with cancellation, so multi-request operations can progress even under a one-request-per-window budget. Missing Redis fails before HTTP dispatch with a definite, safely retryable error. OAuth login traffic also needs the product's normal authentication rate controls.
|
|
40
|
+
|
|
41
|
+
The explicit runtime removes expired authorization flows and superseded unreferenced revoked grants. Failed/blocked/unknown content payloads expire after seven days by default (`failedPayloadRetentionMs`); expired input cannot be automatically replayed. Revocation work is exempt. Never log credentials, request bodies or original content. Logs expose bounded error codes and provider identifiers only.
|
|
42
|
+
|
|
43
|
+
## Development and verification
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
pnpm install --frozen-lockfile
|
|
47
|
+
pnpm typecheck
|
|
48
|
+
pnpm test
|
|
49
|
+
pnpm build
|
|
50
|
+
pnpm pack --pack-destination .artifacts
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Database tests require an explicitly supplied `SDK_TEST_DATABASE_URL` pointing to a local database named `integration_sdk_test`; they reset only SDK test tables in that database. Run `pnpm test:integration`. Packed ESM/CommonJS boundary tests require `SDK_PACKAGE_ARCHIVE=/absolute/path/mycreat-labs-integrations-sdk-0.1.0.tgz pnpm exec vitest run test/providers.package.test.ts`.
|
|
54
|
+
|
|
55
|
+
See [security and provider references](docs/security.md), [migration guidance](docs/migration.md), and [verification record](docs/verification.md). Mocked protocols and local builds do not prove real OAuth consent, third-party writes, native device acceptance or production deployment.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/adapters/bullmq.ts
|
|
21
|
+
var bullmq_exports = {};
|
|
22
|
+
__export(bullmq_exports, {
|
|
23
|
+
createBullMqTransport: () => createBullMqTransport
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(bullmq_exports);
|
|
26
|
+
var import_bullmq = require("bullmq");
|
|
27
|
+
|
|
28
|
+
// src/errors.ts
|
|
29
|
+
var IntegrationError = class extends Error {
|
|
30
|
+
constructor(code, disposition = "permanent_failure", retryAfterMs) {
|
|
31
|
+
super(code);
|
|
32
|
+
this.code = code;
|
|
33
|
+
this.disposition = disposition;
|
|
34
|
+
this.retryAfterMs = retryAfterMs;
|
|
35
|
+
this.name = "IntegrationError";
|
|
36
|
+
}
|
|
37
|
+
code;
|
|
38
|
+
disposition;
|
|
39
|
+
retryAfterMs;
|
|
40
|
+
sdkError = "@mycreat-labs/integrations-sdk/v1";
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/adapters/bullmq.ts
|
|
44
|
+
function createBullMqTransport(options) {
|
|
45
|
+
if (!/^[a-zA-Z0-9_-]{1,128}$/.test(options.prefix))
|
|
46
|
+
throw new IntegrationError("invalid_queue_prefix");
|
|
47
|
+
const name = options.queueName ?? "integration-operations";
|
|
48
|
+
if (!/^[a-zA-Z0-9_-]{1,128}$/.test(name))
|
|
49
|
+
throw new IntegrationError("invalid_queue_name");
|
|
50
|
+
let queue;
|
|
51
|
+
let worker;
|
|
52
|
+
return {
|
|
53
|
+
async publish(messages) {
|
|
54
|
+
if (!queue) throw new IntegrationError("queue_not_started");
|
|
55
|
+
for (const message of messages) {
|
|
56
|
+
const id = `${message.taskId}-${message.version}`;
|
|
57
|
+
const previous = await queue.getJob(id);
|
|
58
|
+
if (previous && ["completed", "failed"].includes(await previous.getState()))
|
|
59
|
+
await previous.remove();
|
|
60
|
+
}
|
|
61
|
+
if (messages.length)
|
|
62
|
+
await queue.addBulk(
|
|
63
|
+
messages.map((data) => ({
|
|
64
|
+
name: "operation",
|
|
65
|
+
data,
|
|
66
|
+
opts: {
|
|
67
|
+
jobId: `${data.taskId}-${data.version}`,
|
|
68
|
+
removeOnComplete: 1e3,
|
|
69
|
+
removeOnFail: 1e3
|
|
70
|
+
}
|
|
71
|
+
}))
|
|
72
|
+
);
|
|
73
|
+
},
|
|
74
|
+
async start(handler) {
|
|
75
|
+
if (worker) return;
|
|
76
|
+
queue = new import_bullmq.Queue(name, {
|
|
77
|
+
connection: options.connection,
|
|
78
|
+
prefix: options.prefix
|
|
79
|
+
});
|
|
80
|
+
worker = new import_bullmq.Worker(
|
|
81
|
+
name,
|
|
82
|
+
async (job) => {
|
|
83
|
+
const data = job.data;
|
|
84
|
+
if (typeof data?.taskId !== "string" || !Number.isSafeInteger(data.version) || data.version < 1)
|
|
85
|
+
return;
|
|
86
|
+
await handler(data);
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
connection: options.connection,
|
|
90
|
+
prefix: options.prefix,
|
|
91
|
+
concurrency: options.concurrency ?? 2
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
worker.on("error", () => void 0);
|
|
95
|
+
queue.on("error", () => void 0);
|
|
96
|
+
await worker.waitUntilReady();
|
|
97
|
+
},
|
|
98
|
+
async stop() {
|
|
99
|
+
await worker?.close();
|
|
100
|
+
await queue?.close();
|
|
101
|
+
worker = void 0;
|
|
102
|
+
queue = void 0;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
107
|
+
0 && (module.exports = {
|
|
108
|
+
createBullMqTransport
|
|
109
|
+
});
|
|
110
|
+
//# sourceMappingURL=bullmq.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/bullmq.ts","../../src/errors.ts"],"sourcesContent":["import { Queue, Worker } from \"bullmq\";\nimport type Redis from \"ioredis\";\nimport { IntegrationError } from \"../errors\";\nimport type { QueueTransport, TaskMessage } from \"../types\";\n\nexport function createBullMqTransport(options: {\n connection: Redis;\n prefix: string;\n queueName?: string;\n concurrency?: number;\n}): QueueTransport {\n if (!/^[a-zA-Z0-9_-]{1,128}$/.test(options.prefix))\n throw new IntegrationError(\"invalid_queue_prefix\");\n const name = options.queueName ?? \"integration-operations\";\n if (!/^[a-zA-Z0-9_-]{1,128}$/.test(name))\n throw new IntegrationError(\"invalid_queue_name\");\n let queue: Queue<TaskMessage> | undefined;\n let worker: Worker<TaskMessage> | undefined;\n return {\n async publish(messages) {\n if (!queue) throw new IntegrationError(\"queue_not_started\");\n for (const message of messages) {\n const id = `${message.taskId}-${message.version}`;\n const previous = await queue.getJob(id);\n if (\n previous &&\n [\"completed\", \"failed\"].includes(await previous.getState())\n )\n await previous.remove();\n }\n if (messages.length)\n await queue.addBulk(\n messages.map((data) => ({\n name: \"operation\",\n data,\n opts: {\n jobId: `${data.taskId}-${data.version}`,\n removeOnComplete: 1000,\n removeOnFail: 1000,\n },\n })),\n );\n },\n async start(handler) {\n if (worker) return;\n queue = new Queue<TaskMessage>(name, {\n connection: options.connection,\n prefix: options.prefix,\n });\n worker = new Worker<TaskMessage>(\n name,\n async (job) => {\n const data = job.data;\n if (\n typeof data?.taskId !== \"string\" ||\n !Number.isSafeInteger(data.version) ||\n data.version < 1\n )\n return;\n await handler(data);\n },\n {\n connection: options.connection,\n prefix: options.prefix,\n concurrency: options.concurrency ?? 2,\n },\n );\n // Runtime health reports queue errors; never emit unhandled EventEmitter errors.\n worker.on(\"error\", () => undefined);\n queue.on(\"error\", () => undefined);\n await worker.waitUntilReady();\n },\n async stop() {\n await worker?.close();\n await queue?.close();\n worker = undefined;\n queue = undefined;\n },\n };\n}\n","export type FailureDisposition =\n \"retryable\" | \"requires_reauth\" | \"outcome_unknown\" | \"permanent_failure\";\nexport class IntegrationError extends Error {\n readonly sdkError = \"@mycreat-labs/integrations-sdk/v1\";\n constructor(\n readonly code: string,\n readonly disposition: FailureDisposition = \"permanent_failure\",\n readonly retryAfterMs?: number,\n ) {\n super(code);\n this.name = \"IntegrationError\";\n }\n}\nexport function integrationError(error: unknown): IntegrationError {\n if (error instanceof IntegrationError) return error;\n // Bundled subpath entries and independently installed copies have separate class identities.\n if (\n error instanceof Error &&\n \"sdkError\" in error &&\n error.sdkError === \"@mycreat-labs/integrations-sdk/v1\" &&\n \"code\" in error &&\n typeof error.code === \"string\" &&\n /^[a-zA-Z0-9_]{1,100}$/.test(error.code) &&\n \"disposition\" in error &&\n [\n \"retryable\",\n \"requires_reauth\",\n \"outcome_unknown\",\n \"permanent_failure\",\n ].includes(String(error.disposition))\n ) {\n const delay =\n \"retryAfterMs\" in error &&\n typeof error.retryAfterMs === \"number\" &&\n Number.isFinite(error.retryAfterMs) &&\n error.retryAfterMs >= 0\n ? error.retryAfterMs\n : undefined;\n return new IntegrationError(\n error.code,\n error.disposition as FailureDisposition,\n delay,\n );\n }\n return new IntegrationError(\"provider_unavailable\", \"outcome_unknown\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAA8B;;;ACEvB,IAAM,mBAAN,cAA+B,MAAM;AAAA,EAE1C,YACW,MACA,cAAkC,qBAClC,cACT;AACA,UAAM,IAAI;AAJD;AACA;AACA;AAGT,SAAK,OAAO;AAAA,EACd;AAAA,EANW;AAAA,EACA;AAAA,EACA;AAAA,EAJF,WAAW;AAStB;;;ADPO,SAAS,sBAAsB,SAKnB;AACjB,MAAI,CAAC,yBAAyB,KAAK,QAAQ,MAAM;AAC/C,UAAM,IAAI,iBAAiB,sBAAsB;AACnD,QAAM,OAAO,QAAQ,aAAa;AAClC,MAAI,CAAC,yBAAyB,KAAK,IAAI;AACrC,UAAM,IAAI,iBAAiB,oBAAoB;AACjD,MAAI;AACJ,MAAI;AACJ,SAAO;AAAA,IACL,MAAM,QAAQ,UAAU;AACtB,UAAI,CAAC,MAAO,OAAM,IAAI,iBAAiB,mBAAmB;AAC1D,iBAAW,WAAW,UAAU;AAC9B,cAAM,KAAK,GAAG,QAAQ,MAAM,IAAI,QAAQ,OAAO;AAC/C,cAAM,WAAW,MAAM,MAAM,OAAO,EAAE;AACtC,YACE,YACA,CAAC,aAAa,QAAQ,EAAE,SAAS,MAAM,SAAS,SAAS,CAAC;AAE1D,gBAAM,SAAS,OAAO;AAAA,MAC1B;AACA,UAAI,SAAS;AACX,cAAM,MAAM;AAAA,UACV,SAAS,IAAI,CAAC,UAAU;AAAA,YACtB,MAAM;AAAA,YACN;AAAA,YACA,MAAM;AAAA,cACJ,OAAO,GAAG,KAAK,MAAM,IAAI,KAAK,OAAO;AAAA,cACrC,kBAAkB;AAAA,cAClB,cAAc;AAAA,YAChB;AAAA,UACF,EAAE;AAAA,QACJ;AAAA,IACJ;AAAA,IACA,MAAM,MAAM,SAAS;AACnB,UAAI,OAAQ;AACZ,cAAQ,IAAI,oBAAmB,MAAM;AAAA,QACnC,YAAY,QAAQ;AAAA,QACpB,QAAQ,QAAQ;AAAA,MAClB,CAAC;AACD,eAAS,IAAI;AAAA,QACX;AAAA,QACA,OAAO,QAAQ;AACb,gBAAM,OAAO,IAAI;AACjB,cACE,OAAO,MAAM,WAAW,YACxB,CAAC,OAAO,cAAc,KAAK,OAAO,KAClC,KAAK,UAAU;AAEf;AACF,gBAAM,QAAQ,IAAI;AAAA,QACpB;AAAA,QACA;AAAA,UACE,YAAY,QAAQ;AAAA,UACpB,QAAQ,QAAQ;AAAA,UAChB,aAAa,QAAQ,eAAe;AAAA,QACtC;AAAA,MACF;AAEA,aAAO,GAAG,SAAS,MAAM,MAAS;AAClC,YAAM,GAAG,SAAS,MAAM,MAAS;AACjC,YAAM,OAAO,eAAe;AAAA,IAC9B;AAAA,IACA,MAAM,OAAO;AACX,YAAM,QAAQ,MAAM;AACpB,YAAM,OAAO,MAAM;AACnB,eAAS;AACT,cAAQ;AAAA,IACV;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import Redis from 'ioredis';
|
|
2
|
+
import { Q as QueueTransport } from '../types-Y4-DoJ1A.cjs';
|
|
3
|
+
|
|
4
|
+
declare function createBullMqTransport(options: {
|
|
5
|
+
connection: Redis;
|
|
6
|
+
prefix: string;
|
|
7
|
+
queueName?: string;
|
|
8
|
+
concurrency?: number;
|
|
9
|
+
}): QueueTransport;
|
|
10
|
+
|
|
11
|
+
export { createBullMqTransport };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import Redis from 'ioredis';
|
|
2
|
+
import { Q as QueueTransport } from '../types-Y4-DoJ1A.js';
|
|
3
|
+
|
|
4
|
+
declare function createBullMqTransport(options: {
|
|
5
|
+
connection: Redis;
|
|
6
|
+
prefix: string;
|
|
7
|
+
queueName?: string;
|
|
8
|
+
concurrency?: number;
|
|
9
|
+
}): QueueTransport;
|
|
10
|
+
|
|
11
|
+
export { createBullMqTransport };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// src/adapters/bullmq.ts
|
|
2
|
+
import { Queue, Worker } from "bullmq";
|
|
3
|
+
|
|
4
|
+
// src/errors.ts
|
|
5
|
+
var IntegrationError = class extends Error {
|
|
6
|
+
constructor(code, disposition = "permanent_failure", retryAfterMs) {
|
|
7
|
+
super(code);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.disposition = disposition;
|
|
10
|
+
this.retryAfterMs = retryAfterMs;
|
|
11
|
+
this.name = "IntegrationError";
|
|
12
|
+
}
|
|
13
|
+
code;
|
|
14
|
+
disposition;
|
|
15
|
+
retryAfterMs;
|
|
16
|
+
sdkError = "@mycreat-labs/integrations-sdk/v1";
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// src/adapters/bullmq.ts
|
|
20
|
+
function createBullMqTransport(options) {
|
|
21
|
+
if (!/^[a-zA-Z0-9_-]{1,128}$/.test(options.prefix))
|
|
22
|
+
throw new IntegrationError("invalid_queue_prefix");
|
|
23
|
+
const name = options.queueName ?? "integration-operations";
|
|
24
|
+
if (!/^[a-zA-Z0-9_-]{1,128}$/.test(name))
|
|
25
|
+
throw new IntegrationError("invalid_queue_name");
|
|
26
|
+
let queue;
|
|
27
|
+
let worker;
|
|
28
|
+
return {
|
|
29
|
+
async publish(messages) {
|
|
30
|
+
if (!queue) throw new IntegrationError("queue_not_started");
|
|
31
|
+
for (const message of messages) {
|
|
32
|
+
const id = `${message.taskId}-${message.version}`;
|
|
33
|
+
const previous = await queue.getJob(id);
|
|
34
|
+
if (previous && ["completed", "failed"].includes(await previous.getState()))
|
|
35
|
+
await previous.remove();
|
|
36
|
+
}
|
|
37
|
+
if (messages.length)
|
|
38
|
+
await queue.addBulk(
|
|
39
|
+
messages.map((data) => ({
|
|
40
|
+
name: "operation",
|
|
41
|
+
data,
|
|
42
|
+
opts: {
|
|
43
|
+
jobId: `${data.taskId}-${data.version}`,
|
|
44
|
+
removeOnComplete: 1e3,
|
|
45
|
+
removeOnFail: 1e3
|
|
46
|
+
}
|
|
47
|
+
}))
|
|
48
|
+
);
|
|
49
|
+
},
|
|
50
|
+
async start(handler) {
|
|
51
|
+
if (worker) return;
|
|
52
|
+
queue = new Queue(name, {
|
|
53
|
+
connection: options.connection,
|
|
54
|
+
prefix: options.prefix
|
|
55
|
+
});
|
|
56
|
+
worker = new Worker(
|
|
57
|
+
name,
|
|
58
|
+
async (job) => {
|
|
59
|
+
const data = job.data;
|
|
60
|
+
if (typeof data?.taskId !== "string" || !Number.isSafeInteger(data.version) || data.version < 1)
|
|
61
|
+
return;
|
|
62
|
+
await handler(data);
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
connection: options.connection,
|
|
66
|
+
prefix: options.prefix,
|
|
67
|
+
concurrency: options.concurrency ?? 2
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
worker.on("error", () => void 0);
|
|
71
|
+
queue.on("error", () => void 0);
|
|
72
|
+
await worker.waitUntilReady();
|
|
73
|
+
},
|
|
74
|
+
async stop() {
|
|
75
|
+
await worker?.close();
|
|
76
|
+
await queue?.close();
|
|
77
|
+
worker = void 0;
|
|
78
|
+
queue = void 0;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export {
|
|
83
|
+
createBullMqTransport
|
|
84
|
+
};
|
|
85
|
+
//# sourceMappingURL=bullmq.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/bullmq.ts","../../src/errors.ts"],"sourcesContent":["import { Queue, Worker } from \"bullmq\";\nimport type Redis from \"ioredis\";\nimport { IntegrationError } from \"../errors\";\nimport type { QueueTransport, TaskMessage } from \"../types\";\n\nexport function createBullMqTransport(options: {\n connection: Redis;\n prefix: string;\n queueName?: string;\n concurrency?: number;\n}): QueueTransport {\n if (!/^[a-zA-Z0-9_-]{1,128}$/.test(options.prefix))\n throw new IntegrationError(\"invalid_queue_prefix\");\n const name = options.queueName ?? \"integration-operations\";\n if (!/^[a-zA-Z0-9_-]{1,128}$/.test(name))\n throw new IntegrationError(\"invalid_queue_name\");\n let queue: Queue<TaskMessage> | undefined;\n let worker: Worker<TaskMessage> | undefined;\n return {\n async publish(messages) {\n if (!queue) throw new IntegrationError(\"queue_not_started\");\n for (const message of messages) {\n const id = `${message.taskId}-${message.version}`;\n const previous = await queue.getJob(id);\n if (\n previous &&\n [\"completed\", \"failed\"].includes(await previous.getState())\n )\n await previous.remove();\n }\n if (messages.length)\n await queue.addBulk(\n messages.map((data) => ({\n name: \"operation\",\n data,\n opts: {\n jobId: `${data.taskId}-${data.version}`,\n removeOnComplete: 1000,\n removeOnFail: 1000,\n },\n })),\n );\n },\n async start(handler) {\n if (worker) return;\n queue = new Queue<TaskMessage>(name, {\n connection: options.connection,\n prefix: options.prefix,\n });\n worker = new Worker<TaskMessage>(\n name,\n async (job) => {\n const data = job.data;\n if (\n typeof data?.taskId !== \"string\" ||\n !Number.isSafeInteger(data.version) ||\n data.version < 1\n )\n return;\n await handler(data);\n },\n {\n connection: options.connection,\n prefix: options.prefix,\n concurrency: options.concurrency ?? 2,\n },\n );\n // Runtime health reports queue errors; never emit unhandled EventEmitter errors.\n worker.on(\"error\", () => undefined);\n queue.on(\"error\", () => undefined);\n await worker.waitUntilReady();\n },\n async stop() {\n await worker?.close();\n await queue?.close();\n worker = undefined;\n queue = undefined;\n },\n };\n}\n","export type FailureDisposition =\n \"retryable\" | \"requires_reauth\" | \"outcome_unknown\" | \"permanent_failure\";\nexport class IntegrationError extends Error {\n readonly sdkError = \"@mycreat-labs/integrations-sdk/v1\";\n constructor(\n readonly code: string,\n readonly disposition: FailureDisposition = \"permanent_failure\",\n readonly retryAfterMs?: number,\n ) {\n super(code);\n this.name = \"IntegrationError\";\n }\n}\nexport function integrationError(error: unknown): IntegrationError {\n if (error instanceof IntegrationError) return error;\n // Bundled subpath entries and independently installed copies have separate class identities.\n if (\n error instanceof Error &&\n \"sdkError\" in error &&\n error.sdkError === \"@mycreat-labs/integrations-sdk/v1\" &&\n \"code\" in error &&\n typeof error.code === \"string\" &&\n /^[a-zA-Z0-9_]{1,100}$/.test(error.code) &&\n \"disposition\" in error &&\n [\n \"retryable\",\n \"requires_reauth\",\n \"outcome_unknown\",\n \"permanent_failure\",\n ].includes(String(error.disposition))\n ) {\n const delay =\n \"retryAfterMs\" in error &&\n typeof error.retryAfterMs === \"number\" &&\n Number.isFinite(error.retryAfterMs) &&\n error.retryAfterMs >= 0\n ? error.retryAfterMs\n : undefined;\n return new IntegrationError(\n error.code,\n error.disposition as FailureDisposition,\n delay,\n );\n }\n return new IntegrationError(\"provider_unavailable\", \"outcome_unknown\");\n}\n"],"mappings":";AAAA,SAAS,OAAO,cAAc;;;ACEvB,IAAM,mBAAN,cAA+B,MAAM;AAAA,EAE1C,YACW,MACA,cAAkC,qBAClC,cACT;AACA,UAAM,IAAI;AAJD;AACA;AACA;AAGT,SAAK,OAAO;AAAA,EACd;AAAA,EANW;AAAA,EACA;AAAA,EACA;AAAA,EAJF,WAAW;AAStB;;;ADPO,SAAS,sBAAsB,SAKnB;AACjB,MAAI,CAAC,yBAAyB,KAAK,QAAQ,MAAM;AAC/C,UAAM,IAAI,iBAAiB,sBAAsB;AACnD,QAAM,OAAO,QAAQ,aAAa;AAClC,MAAI,CAAC,yBAAyB,KAAK,IAAI;AACrC,UAAM,IAAI,iBAAiB,oBAAoB;AACjD,MAAI;AACJ,MAAI;AACJ,SAAO;AAAA,IACL,MAAM,QAAQ,UAAU;AACtB,UAAI,CAAC,MAAO,OAAM,IAAI,iBAAiB,mBAAmB;AAC1D,iBAAW,WAAW,UAAU;AAC9B,cAAM,KAAK,GAAG,QAAQ,MAAM,IAAI,QAAQ,OAAO;AAC/C,cAAM,WAAW,MAAM,MAAM,OAAO,EAAE;AACtC,YACE,YACA,CAAC,aAAa,QAAQ,EAAE,SAAS,MAAM,SAAS,SAAS,CAAC;AAE1D,gBAAM,SAAS,OAAO;AAAA,MAC1B;AACA,UAAI,SAAS;AACX,cAAM,MAAM;AAAA,UACV,SAAS,IAAI,CAAC,UAAU;AAAA,YACtB,MAAM;AAAA,YACN;AAAA,YACA,MAAM;AAAA,cACJ,OAAO,GAAG,KAAK,MAAM,IAAI,KAAK,OAAO;AAAA,cACrC,kBAAkB;AAAA,cAClB,cAAc;AAAA,YAChB;AAAA,UACF,EAAE;AAAA,QACJ;AAAA,IACJ;AAAA,IACA,MAAM,MAAM,SAAS;AACnB,UAAI,OAAQ;AACZ,cAAQ,IAAI,MAAmB,MAAM;AAAA,QACnC,YAAY,QAAQ;AAAA,QACpB,QAAQ,QAAQ;AAAA,MAClB,CAAC;AACD,eAAS,IAAI;AAAA,QACX;AAAA,QACA,OAAO,QAAQ;AACb,gBAAM,OAAO,IAAI;AACjB,cACE,OAAO,MAAM,WAAW,YACxB,CAAC,OAAO,cAAc,KAAK,OAAO,KAClC,KAAK,UAAU;AAEf;AACF,gBAAM,QAAQ,IAAI;AAAA,QACpB;AAAA,QACA;AAAA,UACE,YAAY,QAAQ;AAAA,UACpB,QAAQ,QAAQ;AAAA,UAChB,aAAa,QAAQ,eAAe;AAAA,QACtC;AAAA,MACF;AAEA,aAAO,GAAG,SAAS,MAAM,MAAS;AAClC,YAAM,GAAG,SAAS,MAAM,MAAS;AACjC,YAAM,OAAO,eAAe;AAAA,IAC9B;AAAA,IACA,MAAM,OAAO;AACX,YAAM,QAAQ,MAAM;AACpB,YAAM,OAAO,MAAM;AACnB,eAAS;AACT,cAAQ;AAAA,IACV;AAAA,EACF;AACF;","names":[]}
|