@opencoredev/social-sdk 0.1.2 → 0.2.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.
Files changed (82) hide show
  1. package/README.md +6 -0
  2. package/dist/cli.d.ts +13 -0
  3. package/dist/cli.js +234 -0
  4. package/dist/cloud/common.d.ts +25 -0
  5. package/dist/cloud/common.js +334 -0
  6. package/dist/cloud/lifecycle.d.ts +10 -0
  7. package/dist/cloud/lifecycle.js +112 -0
  8. package/dist/cloud/media.d.ts +23 -0
  9. package/dist/cloud/media.js +100 -0
  10. package/dist/cloud/outcomes.d.ts +9 -0
  11. package/dist/cloud/outcomes.js +195 -0
  12. package/dist/cloud/post-for-me.d.ts +69 -0
  13. package/dist/cloud/post-for-me.js +396 -0
  14. package/dist/cloud/zernio.d.ts +111 -0
  15. package/dist/cloud/zernio.js +632 -0
  16. package/dist/core/adapter.d.ts +158 -0
  17. package/dist/core/adapter.js +3 -0
  18. package/dist/core/client.d.ts +141 -0
  19. package/dist/core/client.js +1285 -0
  20. package/dist/core/concurrency.d.ts +9 -0
  21. package/dist/core/concurrency.js +79 -0
  22. package/dist/core/errors.d.ts +44 -0
  23. package/dist/core/errors.js +50 -0
  24. package/dist/core/idempotency.d.ts +33 -0
  25. package/dist/core/idempotency.js +58 -0
  26. package/dist/core/index.d.ts +6 -0
  27. package/dist/core/index.js +6 -0
  28. package/dist/core/pagination.d.ts +11 -0
  29. package/dist/core/pagination.js +81 -0
  30. package/dist/core/types.d.ts +396 -0
  31. package/dist/core/types.js +16 -0
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +1 -0
  34. package/dist/platforms/bluesky.d.ts +261 -0
  35. package/dist/platforms/bluesky.js +1776 -0
  36. package/dist/platforms/instagram.d.ts +129 -0
  37. package/dist/platforms/instagram.js +1031 -0
  38. package/dist/platforms/linkedin.d.ts +108 -0
  39. package/dist/platforms/linkedin.js +890 -0
  40. package/dist/platforms/threads.d.ts +134 -0
  41. package/dist/platforms/threads.js +945 -0
  42. package/dist/platforms/tiktok.d.ts +31 -0
  43. package/dist/platforms/tiktok.js +593 -0
  44. package/dist/platforms/x-engagement.d.ts +16 -0
  45. package/dist/platforms/x-engagement.js +50 -0
  46. package/dist/platforms/x-text.d.ts +2 -0
  47. package/dist/platforms/x-text.js +123 -0
  48. package/dist/platforms/x-tlds.d.ts +1 -0
  49. package/dist/platforms/x-tlds.js +2 -0
  50. package/dist/platforms/x.d.ts +299 -0
  51. package/dist/platforms/x.js +1287 -0
  52. package/dist/platforms/youtube-upload.d.ts +32 -0
  53. package/dist/platforms/youtube-upload.js +296 -0
  54. package/dist/platforms/youtube.d.ts +108 -0
  55. package/dist/platforms/youtube.js +1100 -0
  56. package/dist/server/connections.d.ts +123 -0
  57. package/dist/server/connections.js +335 -0
  58. package/dist/server/credentials.d.ts +51 -0
  59. package/dist/server/credentials.js +108 -0
  60. package/dist/server/index.d.ts +4 -0
  61. package/dist/server/index.js +4 -0
  62. package/dist/server/oauth.d.ts +52 -0
  63. package/dist/server/oauth.js +553 -0
  64. package/dist/server/webhooks.d.ts +57 -0
  65. package/dist/server/webhooks.js +204 -0
  66. package/dist/testing/index.d.ts +46 -0
  67. package/dist/testing/index.js +564 -0
  68. package/dist/testing.d.ts +1 -0
  69. package/dist/testing.js +1 -0
  70. package/dist/transport/binary.d.ts +2 -0
  71. package/dist/transport/binary.js +29 -0
  72. package/dist/transport/budget.d.ts +3 -0
  73. package/dist/transport/budget.js +26 -0
  74. package/dist/transport/http.d.ts +44 -0
  75. package/dist/transport/http.js +259 -0
  76. package/dist/transport/json.d.ts +8 -0
  77. package/dist/transport/json.js +16 -0
  78. package/dist/transport/upload.d.ts +25 -0
  79. package/dist/transport/upload.js +153 -0
  80. package/dist/transport/validation.d.ts +5 -0
  81. package/dist/transport/validation.js +24 -0
  82. package/package.json +2 -7
@@ -0,0 +1,9 @@
1
+ export interface ConcurrencyLimiterOptions {
2
+ readonly maxActive: number;
3
+ readonly maxQueued: number;
4
+ readonly backend?: string;
5
+ }
6
+ type Work<T> = () => Promise<T>;
7
+ /** A client-owned FIFO budget for one backend instance. */
8
+ export declare function createConcurrencyLimiter(options: ConcurrencyLimiterOptions): <T>(signal: AbortSignal | undefined, operation: string, work: Work<T>) => Promise<T>;
9
+ export {};
@@ -0,0 +1,79 @@
1
+ import { SocialError } from "./errors.js";
2
+ /** A client-owned FIFO budget for one backend instance. */
3
+ export function createConcurrencyLimiter(options) {
4
+ let active = 0;
5
+ const waiting = [];
6
+ async function acquire(signal, operation) {
7
+ if (signal?.aborted)
8
+ throw cancelled(operation);
9
+ if (active < options.maxActive) {
10
+ active += 1;
11
+ return release;
12
+ }
13
+ if (waiting.length >= options.maxQueued) {
14
+ throw saturated(operation, options.backend);
15
+ }
16
+ await new Promise((resolve, reject) => {
17
+ const waiter = {
18
+ resolve,
19
+ reject,
20
+ signal,
21
+ abort: () => {
22
+ if (waiter.settled)
23
+ return;
24
+ waiter.settled = true;
25
+ const index = waiting.indexOf(waiter);
26
+ if (index !== -1)
27
+ waiting.splice(index, 1);
28
+ signal?.removeEventListener("abort", waiter.abort);
29
+ reject(cancelled(operation));
30
+ },
31
+ settled: false,
32
+ };
33
+ waiting.push(waiter);
34
+ signal?.addEventListener("abort", waiter.abort, { once: true });
35
+ });
36
+ return release;
37
+ function release() {
38
+ const next = waiting.shift();
39
+ if (next === undefined) {
40
+ active -= 1;
41
+ return;
42
+ }
43
+ next.settled = true;
44
+ next.signal?.removeEventListener("abort", next.abort);
45
+ // Keep the slot reserved while the queued continuation resumes.
46
+ next.resolve();
47
+ }
48
+ }
49
+ return async function run(signal, operation, work) {
50
+ const release = await acquire(signal, operation);
51
+ try {
52
+ if (signal?.aborted)
53
+ throw cancelled(operation);
54
+ return await work();
55
+ }
56
+ finally {
57
+ release();
58
+ }
59
+ };
60
+ }
61
+ function cancelled(operation) {
62
+ return new SocialError({
63
+ code: "cancelled",
64
+ operation,
65
+ message: "The operation was cancelled while waiting for backend capacity",
66
+ retryDisposition: { kind: "never" },
67
+ });
68
+ }
69
+ function saturated(operation, backend) {
70
+ const options = {
71
+ code: "rate_limited",
72
+ operation,
73
+ message: "Backend concurrency queue is full",
74
+ retryDisposition: { kind: "after-delay", delayMs: 1000 },
75
+ };
76
+ if (backend !== undefined)
77
+ Object.assign(options, { backend: backend });
78
+ return new SocialError(options);
79
+ }
@@ -0,0 +1,44 @@
1
+ import type { ConnectedAccountRef, JsonObject, PreparationIssue, RetryDisposition } from "./types.js";
2
+ export type SocialErrorCode = "invalid_config" | "invalid_input" | "unsupported_capability" | "missing_permission" | "reconnect_required" | "ineligible_account" | "approval_required" | "rate_limited" | "billing_required" | "media_error" | "not_found" | "gone" | "upstream_failure" | "ambiguous_outcome" | "cancelled" | "timeout" | "runtime_unsupported" | "unauthorized" | "idempotency_conflict";
3
+ export interface SocialErrorOptions {
4
+ readonly code: SocialErrorCode;
5
+ readonly operation: string;
6
+ readonly message: string;
7
+ readonly backend?: string | undefined;
8
+ readonly account?: ConnectedAccountRef | undefined;
9
+ readonly issues?: readonly PreparationIssue[] | undefined;
10
+ readonly correlationId?: string | undefined;
11
+ readonly upstreamStatus?: number | undefined;
12
+ readonly upstreamCode?: string | undefined;
13
+ readonly retryDisposition?: RetryDisposition;
14
+ readonly details?: JsonObject | undefined;
15
+ readonly cause?: unknown;
16
+ }
17
+ export interface SerializedSocialError {
18
+ readonly name: "SocialError";
19
+ readonly code: SocialErrorCode;
20
+ readonly operation: string;
21
+ readonly message: string;
22
+ readonly backend?: string;
23
+ readonly account?: ConnectedAccountRef;
24
+ readonly issues?: readonly PreparationIssue[];
25
+ readonly correlationId?: string;
26
+ readonly upstreamStatus?: number;
27
+ readonly upstreamCode?: string;
28
+ readonly retryDisposition: RetryDisposition;
29
+ readonly details?: JsonObject;
30
+ }
31
+ export declare class SocialError extends Error {
32
+ readonly code: SocialErrorCode;
33
+ readonly operation: string;
34
+ readonly backend: string | undefined;
35
+ readonly account: ConnectedAccountRef | undefined;
36
+ readonly issues: readonly PreparationIssue[] | undefined;
37
+ readonly correlationId: string | undefined;
38
+ readonly upstreamStatus: number | undefined;
39
+ readonly upstreamCode: string | undefined;
40
+ readonly retryDisposition: RetryDisposition;
41
+ readonly details: JsonObject | undefined;
42
+ constructor(options: SocialErrorOptions);
43
+ toJSON(): SerializedSocialError;
44
+ }
@@ -0,0 +1,50 @@
1
+ export class SocialError extends Error {
2
+ code;
3
+ operation;
4
+ backend;
5
+ account;
6
+ issues;
7
+ correlationId;
8
+ upstreamStatus;
9
+ upstreamCode;
10
+ retryDisposition;
11
+ details;
12
+ constructor(options) {
13
+ super(options.message, { cause: options.cause });
14
+ this.name = "SocialError";
15
+ this.code = options.code;
16
+ this.operation = options.operation;
17
+ this.backend = options.backend;
18
+ this.account = options.account;
19
+ this.issues = options.issues;
20
+ this.correlationId = options.correlationId;
21
+ this.upstreamStatus = options.upstreamStatus;
22
+ this.upstreamCode = options.upstreamCode;
23
+ this.retryDisposition = options.retryDisposition ?? { kind: "never" };
24
+ this.details = options.details;
25
+ }
26
+ toJSON() {
27
+ const serialized = {
28
+ name: "SocialError",
29
+ code: this.code,
30
+ operation: this.operation,
31
+ message: this.message,
32
+ retryDisposition: this.retryDisposition,
33
+ };
34
+ if (this.backend !== undefined)
35
+ Object.assign(serialized, { backend: this.backend });
36
+ if (this.account !== undefined)
37
+ Object.assign(serialized, { account: this.account });
38
+ if (this.issues !== undefined)
39
+ Object.assign(serialized, { issues: this.issues });
40
+ if (this.correlationId !== undefined)
41
+ Object.assign(serialized, { correlationId: this.correlationId });
42
+ if (this.upstreamStatus !== undefined)
43
+ Object.assign(serialized, { upstreamStatus: this.upstreamStatus });
44
+ if (this.upstreamCode !== undefined)
45
+ Object.assign(serialized, { upstreamCode: this.upstreamCode });
46
+ if (this.details !== undefined)
47
+ Object.assign(serialized, { details: this.details });
48
+ return serialized;
49
+ }
50
+ }
@@ -0,0 +1,33 @@
1
+ import type { DeliveryOutcome } from "./types.js";
2
+ export interface IdempotencyClaimInput {
3
+ readonly scope: string;
4
+ readonly key: string;
5
+ readonly fingerprint: string;
6
+ readonly targetKeys: readonly string[];
7
+ }
8
+ export type IdempotencyClaim = {
9
+ readonly kind: "new" | "existing";
10
+ readonly claimId: string;
11
+ readonly outcomes: Readonly<Record<string, DeliveryOutcome>>;
12
+ } | {
13
+ readonly kind: "conflict";
14
+ };
15
+ export interface IdempotencyStore {
16
+ /** Atomically creates or reads a logical operation claim. */
17
+ claim(input: IdempotencyClaimInput): Promise<IdempotencyClaim>;
18
+ /** Atomically persists one target outcome without replacing other target outcomes. */
19
+ saveOutcome(input: {
20
+ readonly claimId: string;
21
+ readonly targetKey: string;
22
+ readonly outcome: DeliveryOutcome;
23
+ }): Promise<void>;
24
+ }
25
+ export declare function stableSerialize(value: unknown): string;
26
+ export declare function fingerprint(value: unknown): Promise<string>;
27
+ export declare function deriveTargetIdempotencyKey(input: {
28
+ readonly logicalKey: string;
29
+ readonly scope?: string;
30
+ readonly backend: string;
31
+ readonly targetKey: string;
32
+ readonly payloadFingerprint: string;
33
+ }): Promise<string>;
@@ -0,0 +1,58 @@
1
+ function normalizeForJson(value, seen) {
2
+ if (value === null || typeof value === "string" || typeof value === "boolean")
3
+ return value;
4
+ if (typeof value === "number") {
5
+ if (!Number.isFinite(value))
6
+ throw new TypeError("Idempotency payload numbers must be finite");
7
+ return value;
8
+ }
9
+ if (typeof value === "undefined")
10
+ return undefined;
11
+ if (typeof value === "bigint" || typeof value === "symbol" || typeof value === "function") {
12
+ throw new TypeError("Idempotency payload must be JSON-safe");
13
+ }
14
+ if (Array.isArray(value)) {
15
+ if (seen.has(value))
16
+ throw new TypeError("Idempotency payload must not contain cycles");
17
+ seen.add(value);
18
+ const result = value.map((entry) => normalizeForJson(entry, seen));
19
+ seen.delete(value);
20
+ return result;
21
+ }
22
+ if (typeof Blob !== "undefined" && value instanceof Blob) {
23
+ throw new TypeError("Blob inputs require a caller-provided media fingerprint");
24
+ }
25
+ if (typeof value === "object") {
26
+ if (seen.has(value))
27
+ throw new TypeError("Idempotency payload must not contain cycles");
28
+ seen.add(value);
29
+ const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
30
+ const result = {};
31
+ for (const [key, entry] of entries) {
32
+ const normalized = normalizeForJson(entry, seen);
33
+ if (normalized !== undefined)
34
+ result[key] = normalized;
35
+ }
36
+ seen.delete(value);
37
+ return result;
38
+ }
39
+ throw new TypeError("Unsupported idempotency payload value");
40
+ }
41
+ export function stableSerialize(value) {
42
+ return JSON.stringify(normalizeForJson(value, new Set()));
43
+ }
44
+ function bytesToHex(bytes) {
45
+ let result = "";
46
+ for (const byte of bytes)
47
+ result += byte.toString(16).padStart(2, "0");
48
+ return result;
49
+ }
50
+ export async function fingerprint(value) {
51
+ const bytes = new TextEncoder().encode(stableSerialize(value));
52
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
53
+ return bytesToHex(new Uint8Array(digest));
54
+ }
55
+ export async function deriveTargetIdempotencyKey(input) {
56
+ const digest = await fingerprint(input);
57
+ return `social-${digest}`;
58
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./adapter.js";
2
+ export * from "./client.js";
3
+ export * from "./errors.js";
4
+ export * from "./idempotency.js";
5
+ export * from "./types.js";
6
+ export { iterateItems, type IterationOptions } from "./pagination.js";
@@ -0,0 +1,6 @@
1
+ export * from "./adapter.js";
2
+ export * from "./client.js";
3
+ export * from "./errors.js";
4
+ export * from "./idempotency.js";
5
+ export * from "./types.js";
6
+ export { iterateItems } from "./pagination.js";
@@ -0,0 +1,11 @@
1
+ import type { Page } from "./types.js";
2
+ export interface IterationOptions {
3
+ readonly maxPages?: number;
4
+ readonly maxItems?: number;
5
+ readonly signal?: AbortSignal;
6
+ }
7
+ /** Lazy traversal with explicit bounds, cancellation and repeated-cursor detection. */
8
+ export declare function iterateItems<T>(read: (cursor?: string) => Promise<Page<T>>, options?: IterationOptions): AsyncGenerator<T>;
9
+ /** Cursors bind navigation to its query scope; they are never authorization grants. */
10
+ export declare function encodeCursor(scope: string, value: string): string;
11
+ export declare function decodeCursor(scope: string, cursor: string): string;
@@ -0,0 +1,81 @@
1
+ import { SocialError } from "./errors.js";
2
+ /** Lazy traversal with explicit bounds, cancellation and repeated-cursor detection. */
3
+ export async function* iterateItems(read, options = {}) {
4
+ const maxPages = options.maxPages ?? 100;
5
+ const maxItems = options.maxItems ?? 10_000;
6
+ if (!Number.isSafeInteger(maxPages) ||
7
+ maxPages < 1 ||
8
+ !Number.isSafeInteger(maxItems) ||
9
+ maxItems < 1)
10
+ throw new SocialError({
11
+ code: "invalid_input",
12
+ operation: "pagination",
13
+ message: "Iteration bounds must be positive safe integers",
14
+ });
15
+ const seen = new Set();
16
+ let cursor;
17
+ let count = 0;
18
+ for (let index = 0; index < maxPages; index++) {
19
+ if (options.signal?.aborted)
20
+ throw new SocialError({
21
+ code: "cancelled",
22
+ operation: "pagination",
23
+ message: "Iteration was cancelled",
24
+ });
25
+ const page = await read(cursor);
26
+ for (const item of page.items) {
27
+ if (options.signal?.aborted)
28
+ throw new SocialError({
29
+ code: "cancelled",
30
+ operation: "pagination",
31
+ message: "Iteration was cancelled",
32
+ });
33
+ yield item;
34
+ if (++count >= maxItems)
35
+ return;
36
+ }
37
+ if (page.nextCursor === undefined)
38
+ return;
39
+ if (!page.nextCursor || seen.has(page.nextCursor))
40
+ throw new SocialError({
41
+ code: "upstream_failure",
42
+ operation: "pagination",
43
+ message: "Backend returned an empty or repeated pagination cursor",
44
+ });
45
+ seen.add(page.nextCursor);
46
+ cursor = page.nextCursor;
47
+ }
48
+ }
49
+ /** Cursors bind navigation to its query scope; they are never authorization grants. */
50
+ export function encodeCursor(scope, value) {
51
+ if (!value || value.length > 16_384)
52
+ throw new SocialError({
53
+ code: "upstream_failure",
54
+ operation: "pagination",
55
+ message: "Backend returned an invalid cursor",
56
+ });
57
+ return `social-v1.${encodeURIComponent(JSON.stringify([scope, value]))}`;
58
+ }
59
+ export function decodeCursor(scope, cursor) {
60
+ try {
61
+ if (!cursor.startsWith("social-v1.") || cursor.length > 100_000)
62
+ throw new Error();
63
+ const parsed = JSON.parse(decodeURIComponent(cursor.slice(10)));
64
+ if (!Array.isArray(parsed) ||
65
+ parsed.length !== 2 ||
66
+ parsed[0] !== scope ||
67
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- Validate the untrusted decoded cursor tuple.
68
+ typeof parsed[1] !== "string" ||
69
+ !parsed[1] ||
70
+ parsed[1].length > 16_384)
71
+ throw new Error();
72
+ return parsed[1];
73
+ }
74
+ catch {
75
+ throw new SocialError({
76
+ code: "invalid_input",
77
+ operation: "pagination",
78
+ message: "Cursor does not belong to this backend, tenant, and query",
79
+ });
80
+ }
81
+ }