@opencoredev/social-sdk 0.3.0 → 0.5.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 (65) hide show
  1. package/dist/cli-request.d.ts +15 -0
  2. package/dist/cli-request.js +193 -0
  3. package/dist/cli.d.ts +4 -3
  4. package/dist/cli.js +24 -21
  5. package/dist/cloud/common.d.ts +13 -8
  6. package/dist/cloud/common.js +40 -60
  7. package/dist/cloud/lifecycle.js +31 -35
  8. package/dist/cloud/media.d.ts +6 -2
  9. package/dist/cloud/media.js +50 -37
  10. package/dist/cloud/outcomes.d.ts +4 -3
  11. package/dist/cloud/outcomes.js +8 -15
  12. package/dist/cloud/post-for-me.js +41 -49
  13. package/dist/cloud/postfast.d.ts +54 -0
  14. package/dist/cloud/postfast.js +578 -0
  15. package/dist/cloud/zernio.js +58 -98
  16. package/dist/core/client.js +79 -99
  17. package/dist/core/fields.d.ts +14 -0
  18. package/dist/core/fields.js +14 -0
  19. package/dist/core/idempotency.d.ts +7 -2
  20. package/dist/core/idempotency.js +37 -20
  21. package/dist/core/pagination.js +8 -7
  22. package/dist/core/types.d.ts +3 -2
  23. package/dist/platforms/bluesky.d.ts +65 -1
  24. package/dist/platforms/bluesky.js +675 -276
  25. package/dist/platforms/instagram.d.ts +2 -0
  26. package/dist/platforms/instagram.js +130 -105
  27. package/dist/platforms/linkedin.d.ts +58 -1
  28. package/dist/platforms/linkedin.js +877 -107
  29. package/dist/platforms/threads.d.ts +13 -1
  30. package/dist/platforms/threads.js +204 -302
  31. package/dist/platforms/tiktok.d.ts +4 -0
  32. package/dist/platforms/tiktok.js +140 -124
  33. package/dist/platforms/webhook-adapter.d.ts +9 -0
  34. package/dist/platforms/webhook-adapter.js +24 -0
  35. package/dist/platforms/x-engagement.js +7 -12
  36. package/dist/platforms/x-stream.d.ts +83 -0
  37. package/dist/platforms/x-stream.js +350 -0
  38. package/dist/platforms/x.d.ts +72 -0
  39. package/dist/platforms/x.js +328 -119
  40. package/dist/platforms/youtube-upload.d.ts +1 -1
  41. package/dist/platforms/youtube-upload.js +6 -2
  42. package/dist/platforms/youtube.d.ts +28 -4
  43. package/dist/platforms/youtube.js +291 -133
  44. package/dist/server/bluesky-oauth.d.ts +177 -0
  45. package/dist/server/bluesky-oauth.js +1229 -0
  46. package/dist/server/connections.d.ts +14 -0
  47. package/dist/server/connections.js +10 -2
  48. package/dist/server/egress.d.ts +14 -0
  49. package/dist/server/egress.js +115 -0
  50. package/dist/server/oauth-internal.d.ts +6 -0
  51. package/dist/server/oauth-internal.js +66 -0
  52. package/dist/server/oauth.d.ts +1 -1
  53. package/dist/server/oauth.js +46 -99
  54. package/dist/server/webhooks.d.ts +136 -3
  55. package/dist/server/webhooks.js +639 -25
  56. package/dist/testing/index.js +14 -28
  57. package/dist/transport/http.d.ts +1 -1
  58. package/dist/transport/http.js +0 -1
  59. package/dist/transport/json.d.ts +7 -0
  60. package/dist/transport/json.js +32 -4
  61. package/dist/transport/upload.d.ts +1 -1
  62. package/dist/transport/upload.js +46 -38
  63. package/dist/transport/validation.d.ts +16 -5
  64. package/dist/transport/validation.js +29 -7
  65. package/package.json +6 -2
@@ -1,5 +1,10 @@
1
- /* oxlint-disable anti-slop/require-readable-spacing -- deterministic mock adapter fixtures stay grouped by surface. */
2
1
  import { SocialError, connectedAccountRef, defineAdapter, platformPostRef, } from "../core/index.js";
2
+ import { definedFields } from "../core/fields.js";
3
+ import { isJsonValue } from "../transport/json.js";
4
+ import { isJsonObject } from "../transport/validation.js";
5
+ function isJsonPrimitive(value) {
6
+ return value === null || typeof value !== "object";
7
+ }
3
8
  const encoder = new TextEncoder();
4
9
  const decoder = new TextDecoder();
5
10
  function mockManifest() {
@@ -87,12 +92,7 @@ export function mockBackend(options = {}) {
87
92
  sequence: ++sequence,
88
93
  operation,
89
94
  backend: context.backendInstance,
90
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
91
- ...(account === undefined ? {} : { account }),
92
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
93
- ...(context.targetIdempotencyKey === undefined
94
- ? {}
95
- : { idempotencyKey: context.targetIdempotencyKey }),
95
+ ...definedFields({ account, idempotencyKey: context.targetIdempotencyKey }),
96
96
  });
97
97
  }
98
98
  const testing = {
@@ -111,7 +111,7 @@ export function mockBackend(options = {}) {
111
111
  },
112
112
  webhookFixtures: webhooks,
113
113
  };
114
- const adapter = defineAdapter({
114
+ return defineAdapter({
115
115
  id: "mock",
116
116
  capabilities: mockManifest(),
117
117
  testing,
@@ -466,20 +466,17 @@ export function mockBackend(options = {}) {
466
466
  webhooks: {
467
467
  async verify(input, context) {
468
468
  record("webhooks.verify", context);
469
+ const valid = input.headers.get("x-mock-signature") === "valid";
469
470
  return {
470
- valid: input.headers.get("x-mock-signature") === "valid",
471
+ valid,
471
472
  method: "mock",
472
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
473
- ...(input.headers.get("x-mock-signature") === "valid"
474
- ? {}
475
- : { reason: "Invalid mock signature" }),
473
+ ...definedFields({ reason: valid ? undefined : "Invalid mock signature" }),
476
474
  };
477
475
  },
478
476
  async decode(input, context) {
479
477
  record("webhooks.decode", context);
480
478
  const parsed = JSON.parse(decoder.decode(input.body));
481
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
482
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
479
+ if (!isJsonValue(parsed) || !isJsonObject(parsed)) {
483
480
  throw new SocialError({
484
481
  code: "invalid_input",
485
482
  operation: "webhooks.decode",
@@ -488,24 +485,14 @@ export function mockBackend(options = {}) {
488
485
  }
489
486
  const entries = [];
490
487
  for (const [key, value] of Object.entries(parsed)) {
491
- if (
492
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
493
- typeof value === "string" ||
494
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
495
- typeof value === "number" ||
496
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
497
- typeof value === "boolean" ||
498
- value === null) {
488
+ if (isJsonPrimitive(value))
499
489
  entries.push([key, value]);
500
- }
501
490
  }
502
491
  return Object.fromEntries(entries);
503
492
  },
504
493
  },
505
494
  native: { scenario: () => scenario },
506
495
  });
507
- // SAFETY: defineAdapter preserves the supplied testing controller and native mock shape.
508
- return adapter;
509
496
  }
510
497
  export class MemoryIdempotencyStore {
511
498
  #claimsByKey = new Map();
@@ -556,8 +543,7 @@ export function mockSharedAccountAuthorization(input) {
556
543
  return accounts.map((account) => ({
557
544
  account,
558
545
  allowed: tenant !== undefined && (input.memberships[tenant]?.includes(account.accountId) ?? false),
559
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
560
- ...(tenant === undefined ? { reason: "A tenant is required" } : {}),
546
+ ...definedFields({ reason: tenant === undefined ? "A tenant is required" : undefined }),
561
547
  }));
562
548
  },
563
549
  };
@@ -41,4 +41,4 @@ export declare function retryDelay(value: string | null, now: number): number |
41
41
  /** Enforce cancellation even when an injected I/O implementation ignores signals. */
42
42
  export declare function abortable<T>(work: Promise<T>, signal: AbortSignal): Promise<T>;
43
43
  export declare function readJson(response: Response, limit: number, signal?: AbortSignal): Promise<JsonValue>;
44
- export declare function createHttp(options?: HttpOptions): (input: HttpRequest) => Promise<unknown>;
44
+ export declare function createHttp(options?: HttpOptions): (input: HttpRequest) => Promise<JsonValue>;
@@ -238,7 +238,6 @@ export function createHttp(options = {}) {
238
238
  throw new HttpError("Upstream request failed with HTTP 429.", "http", dispatched, status, delay);
239
239
  throw new HttpError("Retry delay exceeds the request deadline.", "timeout", dispatched, status, delay, lastRequestId);
240
240
  }
241
- // oxlint-disable-next-line anti-slop/require-readable-spacing -- await is the guarded retry step.
242
241
  await abortable((options.sleep ?? sleep)(delay, controller.signal), controller.signal);
243
242
  }
244
243
  }
@@ -2,6 +2,13 @@
2
2
  export type JsonValue = null | boolean | number | string | JsonValue[] | {
3
3
  [key: string]: JsonValue;
4
4
  };
5
+ /**
6
+ * True when every member of `value` is a JSON primitive, array, or plain object.
7
+ * Object properties set to `undefined` pass because reading them matches an
8
+ * absent key. `NaN`, `Infinity`, class instances such as `Date`, and cycles are
9
+ * rejected because `JSON.stringify` would change or drop them.
10
+ */
11
+ export declare function isJsonValue(value: unknown, ancestors?: Set<object>): value is JsonValue;
5
12
  export declare function parseJson(text: string): JsonValue;
6
13
  export declare function jsonObject(value: JsonValue): {
7
14
  readonly [key: string]: JsonValue;
@@ -1,3 +1,26 @@
1
+ /**
2
+ * True when every member of `value` is a JSON primitive, array, or plain object.
3
+ * Object properties set to `undefined` pass because reading them matches an
4
+ * absent key. `NaN`, `Infinity`, class instances such as `Date`, and cycles are
5
+ * rejected because `JSON.stringify` would change or drop them.
6
+ */
7
+ export function isJsonValue(value, ancestors = new Set()) {
8
+ if (value === null || typeof value === "string" || typeof value === "boolean")
9
+ return true;
10
+ if (typeof value === "number")
11
+ return Number.isFinite(value);
12
+ if (typeof value !== "object" || ancestors.has(value))
13
+ return false;
14
+ const prototype = Object.getPrototypeOf(value);
15
+ if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null)
16
+ return false;
17
+ ancestors.add(value);
18
+ const valid = Array.isArray(value)
19
+ ? value.every((item) => isJsonValue(item, ancestors))
20
+ : Object.values(value).every((item) => item === undefined || isJsonValue(item, ancestors));
21
+ ancestors.delete(value);
22
+ return valid;
23
+ }
1
24
  export function parseJson(text) {
2
25
  // Match quoted strings first so digits within strings are never rewritten.
3
26
  const lossless = text.replace(/"(?:[^"\\]|\\[\s\S])*"|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/g, (token) => {
@@ -5,12 +28,17 @@ export function parseJson(text) {
5
28
  return token;
6
29
  return `"${token}"`;
7
30
  });
8
- // SAFETY: JSON.parse returns the recursive JSON grammar represented by JsonValue.
9
- return JSON.parse(lossless);
31
+ const parsed = JSON.parse(lossless);
32
+ // JSON.parse only yields this grammar; the check proves it to the type system.
33
+ if (!isJsonValue(parsed))
34
+ throw new SyntaxError("Expected a JSON value");
35
+ return parsed;
36
+ }
37
+ function isJsonObjectValue(value) {
38
+ return value !== null && !Array.isArray(value) && typeof value === "object";
10
39
  }
11
40
  export function jsonObject(value) {
12
- if (value === null || Array.isArray(value) || Object(value) !== value)
41
+ if (!isJsonObjectValue(value))
13
42
  throw new TypeError("Expected a JSON object");
14
- // SAFETY: Object(value) === value and Array.isArray(value) is false establish a JSON object.
15
43
  return value;
16
44
  }
@@ -2,7 +2,7 @@ export interface UploadSource {
2
2
  mimeType: string;
3
3
  size?: number;
4
4
  body?: Blob;
5
- /** Called exactly once per explicit upload. Return a fresh stream each time. */
5
+ /** Called at most once per explicit upload, and not at all when `body` is set. Return a fresh stream each time. */
6
6
  open: () => ReadableStream<Uint8Array>;
7
7
  }
8
8
  export interface UploadOptions {
@@ -60,50 +60,55 @@ export async function upload(options) {
60
60
  let buffered;
61
61
  let bufferedOffset = 0;
62
62
  let bytes = 0;
63
+ let sourceEnded = false;
63
64
  let dispatched = false;
64
65
  try {
65
- reader = source.open().getReader();
66
- const sourceReader = reader;
67
- const bodyStream = new ReadableStream({
68
- async pull(output) {
69
- try {
70
- controller.signal.throwIfAborted();
71
- if (buffered === undefined || bufferedOffset >= buffered.byteLength) {
72
- const next = await abortable(sourceReader.read(), controller.signal);
73
- if (next.done) {
74
- if (source.size !== undefined && bytes !== source.size)
75
- throw new HttpError("Upload stream length does not match declared size.", "invalid-input", dispatched);
76
- output.close();
77
- return;
66
+ // fetch sends a Blob body itself. Open and count the source only when streaming, so an
67
+ // unused counting stream cannot pre-read source bytes into the consumed total.
68
+ const openCountingStream = () => {
69
+ reader = source.open().getReader();
70
+ const sourceReader = reader;
71
+ return new ReadableStream({
72
+ async pull(output) {
73
+ try {
74
+ controller.signal.throwIfAborted();
75
+ if (buffered === undefined || bufferedOffset >= buffered.byteLength) {
76
+ const next = await abortable(sourceReader.read(), controller.signal);
77
+ if (next.done) {
78
+ if (source.size !== undefined && bytes !== source.size)
79
+ throw new HttpError("Upload stream length does not match declared size.", "invalid-input", dispatched);
80
+ sourceEnded = true;
81
+ output.close();
82
+ return;
83
+ }
84
+ if (bytes + next.value.byteLength > options.maxBytes) {
85
+ throw new HttpError("Upload stream exceeds its total byte limit.", "invalid-input", dispatched);
86
+ }
87
+ buffered = next.value;
88
+ bufferedOffset = 0;
78
89
  }
79
- if (bytes + next.value.byteLength > options.maxBytes) {
80
- // oxlint-disable-next-line anti-slop/require-readable-spacing -- compact guarded error construction.
81
- throw new HttpError("Upload stream exceeds its total byte limit.", "invalid-input", dispatched);
90
+ const chunk = buffered.subarray(bufferedOffset, Math.min(buffered.byteLength, bufferedOffset + maxChunkBytes));
91
+ bufferedOffset += chunk.byteLength;
92
+ bytes += chunk.byteLength;
93
+ output.enqueue(chunk);
94
+ try {
95
+ options.onProgress?.(bytes);
96
+ }
97
+ catch {
98
+ /* diagnostics cannot alter upload */
82
99
  }
83
- buffered = next.value;
84
- bufferedOffset = 0;
85
- }
86
- const chunk = buffered.subarray(bufferedOffset, Math.min(buffered.byteLength, bufferedOffset + maxChunkBytes));
87
- bufferedOffset += chunk.byteLength;
88
- bytes += chunk.byteLength;
89
- output.enqueue(chunk);
90
- try {
91
- options.onProgress?.(bytes);
92
100
  }
93
- catch {
94
- /* diagnostics cannot alter upload */
101
+ catch (error) {
102
+ void sourceReader.cancel().catch(() => undefined);
103
+ output.error(error);
95
104
  }
96
- }
97
- catch (error) {
98
- void sourceReader.cancel().catch(() => undefined);
99
- output.error(error);
100
- }
101
- },
102
- cancel(reason) {
103
- void sourceReader.cancel(reason).catch(() => undefined);
104
- },
105
- }, { highWaterMark: 1 });
106
- const requestBody = source.body ?? bodyStream;
105
+ },
106
+ cancel(reason) {
107
+ void sourceReader.cancel(reason).catch(() => undefined);
108
+ },
109
+ }, { highWaterMark: 1 });
110
+ };
111
+ const requestBody = source.body ?? openCountingStream();
107
112
  if (source.body !== undefined)
108
113
  bytes = source.body.size;
109
114
  const headers = new Headers({ "Content-Type": source.mimeType });
@@ -130,6 +135,9 @@ export async function upload(options) {
130
135
  throw new HttpError(`Storage upload failed with HTTP ${response.status}.`, "http", true, response.status);
131
136
  if (source.size !== undefined && bytes !== source.size)
132
137
  throw new HttpError("Storage accepted before the full declared upload was consumed.", "invalid-response", true);
138
+ // Without a declared size, only the end of the source proves every byte was sent.
139
+ if (source.body === undefined && source.size === undefined && !sourceEnded)
140
+ throw new HttpError("Storage accepted before the upload stream ended.", "invalid-response", true);
133
141
  const etag = response.headers.get("etag");
134
142
  if (etag === null)
135
143
  return { bytes };
@@ -1,5 +1,16 @@
1
- export declare function object(value: unknown): Record<string, unknown>;
2
- export declare function array(value: unknown): unknown[];
3
- export declare function string(value: unknown): string;
4
- export declare function optionalString(value: unknown): string | undefined;
5
- export declare function optionalNumber(value: unknown): number | undefined;
1
+ import type { JsonObject, JsonValue } from "../core/types.js";
2
+ /** A decoded JSON field; `undefined` when the key is absent from its object. */
3
+ export type JsonField = JsonValue | undefined;
4
+ export declare function isJsonObject(value: JsonField): value is JsonObject;
5
+ export declare function isJsonArray(value: JsonField): value is readonly JsonValue[];
6
+ export declare function isString(value: JsonField): value is string;
7
+ export declare function isFiniteNumber(value: JsonField): value is number;
8
+ export declare function isBoolean(value: JsonField): value is boolean;
9
+ export declare function object(value: JsonField): JsonObject;
10
+ export declare function array(value: JsonField): readonly JsonValue[];
11
+ export declare function string(value: JsonField): string;
12
+ export declare function optionalString(value: JsonField): string | undefined;
13
+ export declare function optionalNumber(value: JsonField): number | undefined;
14
+ export declare function optionalBoolean(value: JsonField): boolean | undefined;
15
+ export declare function optionalObject(value: JsonField): JsonObject | undefined;
16
+ export declare function optionalArray(value: JsonField): readonly JsonValue[] | undefined;
@@ -1,24 +1,46 @@
1
1
  import { HttpError } from "./http.js";
2
- /* oxlint-disable anti-slop/no-unknown-parameters, anti-slop/no-unsafe-dictionary-type, anti-slop/no-runtime-typeof, anti-slop/require-safety-comment-for-type-assertion -- These helpers validate unknown provider responses at the transport boundary. */
2
+ export function isJsonObject(value) {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4
+ }
5
+ export function isJsonArray(value) {
6
+ return Array.isArray(value);
7
+ }
8
+ export function isString(value) {
9
+ return typeof value === "string";
10
+ }
11
+ export function isFiniteNumber(value) {
12
+ return typeof value === "number" && Number.isFinite(value);
13
+ }
14
+ export function isBoolean(value) {
15
+ return typeof value === "boolean";
16
+ }
3
17
  export function object(value) {
4
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
18
+ if (!isJsonObject(value))
5
19
  throw new HttpError("Upstream response must be an object.", "invalid-response", true);
6
- }
7
20
  return value;
8
21
  }
9
22
  export function array(value) {
10
- if (!Array.isArray(value))
23
+ if (!isJsonArray(value))
11
24
  throw new HttpError("Upstream response must contain an array.", "invalid-response", true);
12
25
  return value;
13
26
  }
14
27
  export function string(value) {
15
- if (typeof value !== "string" || !value)
28
+ if (!isString(value) || !value)
16
29
  throw new HttpError("Upstream response is missing a string identifier or required field.", "invalid-response", true);
17
30
  return value;
18
31
  }
19
32
  export function optionalString(value) {
20
- return typeof value === "string" ? value : undefined;
33
+ return isString(value) ? value : undefined;
21
34
  }
22
35
  export function optionalNumber(value) {
23
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
36
+ return isFiniteNumber(value) ? value : undefined;
37
+ }
38
+ export function optionalBoolean(value) {
39
+ return isBoolean(value) ? value : undefined;
40
+ }
41
+ export function optionalObject(value) {
42
+ return isJsonObject(value) ? value : undefined;
43
+ }
44
+ export function optionalArray(value) {
45
+ return isJsonArray(value) ? value : undefined;
24
46
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencoredev/social-sdk",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Social platform integrations for TypeScript applications.",
5
5
  "homepage": "https://github.com/opencoredev/social-sdk#readme",
6
6
  "bugs": {
@@ -41,6 +41,10 @@
41
41
  "types": "./dist/cloud/post-for-me.d.ts",
42
42
  "import": "./dist/cloud/post-for-me.js"
43
43
  },
44
+ "./cloud/postfast": {
45
+ "types": "./dist/cloud/postfast.d.ts",
46
+ "import": "./dist/cloud/postfast.js"
47
+ },
44
48
  "./server": {
45
49
  "types": "./dist/server/index.d.ts",
46
50
  "import": "./dist/server/index.js"
@@ -81,7 +85,7 @@
81
85
  "scripts": {
82
86
  "build": "tsc -p tsconfig.json --noEmitOnError",
83
87
  "prepack": "bun run build",
84
- "check-types": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.type-tests.json",
88
+ "check-types": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.type-tests.json && tsc -p tsconfig.tests.json",
85
89
  "lint": "oxlint src tests type-tests",
86
90
  "test": "bun test tests"
87
91
  },