@volter/twin-linkedin 0.1.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.
- package/LICENSE +202 -0
- package/README.md +39 -0
- package/dist/client/linkedin-mirror.bundle.js +323 -0
- package/dist/client/linkedin-mirror.d.ts +43 -0
- package/dist/client/linkedin-mirror.js +393 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +30 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +74 -0
- package/dist/src/linkedin-budget.d.ts +36 -0
- package/dist/src/linkedin-budget.js +87 -0
- package/dist/src/linkedin-capabilities.d.ts +4 -0
- package/dist/src/linkedin-capabilities.js +1027 -0
- package/dist/src/linkedin-conformance.d.ts +8 -0
- package/dist/src/linkedin-conformance.js +58 -0
- package/dist/src/linkedin-connector.d.ts +66 -0
- package/dist/src/linkedin-connector.js +326 -0
- package/dist/src/linkedin-errors.d.ts +21 -0
- package/dist/src/linkedin-errors.js +38 -0
- package/dist/src/linkedin-media.d.ts +125 -0
- package/dist/src/linkedin-media.js +331 -0
- package/dist/src/linkedin-mirror-ui.d.ts +59 -0
- package/dist/src/linkedin-mirror-ui.js +174 -0
- package/dist/src/linkedin-server.d.ts +10 -0
- package/dist/src/linkedin-server.js +154 -0
- package/dist/src/linkedin-twin.d.ts +23 -0
- package/dist/src/linkedin-twin.js +1220 -0
- package/package.json +58 -0
- package/src/cli.ts +28 -0
- package/src/index.ts +118 -0
- package/src/linkedin-budget.ts +108 -0
- package/src/linkedin-capabilities.ts +1057 -0
- package/src/linkedin-conformance.ts +73 -0
- package/src/linkedin-connector.ts +314 -0
- package/src/linkedin-errors.ts +58 -0
- package/src/linkedin-media.ts +363 -0
- package/src/linkedin-mirror-ui.ts +182 -0
- package/src/linkedin-server.ts +150 -0
- package/src/linkedin-twin.ts +1143 -0
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@volter/twin-linkedin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local LinkedIn twin (organization posts with image, video or article; the Images and Videos upload protocols; admin roles) with a linkedin.com company-page mirror, built on @volter/world-core.",
|
|
5
|
+
"author": "Volter (https://github.com/volter-ai)",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"files": [
|
|
8
|
+
"src",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE",
|
|
11
|
+
"!**/*.test.ts",
|
|
12
|
+
"!**/*.test.tsx",
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/volter-ai/twin.git",
|
|
18
|
+
"directory": "packages/twin/linkedin"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/volter-ai/twin/tree/main/packages/twin/linkedin#readme",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/src/index.d.ts",
|
|
25
|
+
"default": "./dist/src/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"bin": {
|
|
29
|
+
"world-linkedin": "dist/src/cli.js"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"test": "bun test src/*.test.ts",
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"build": "node ../../../scripts/publish/build.mjs",
|
|
35
|
+
"prepack": "node ../../../scripts/publish/prepare-publish.mjs prepack",
|
|
36
|
+
"postpack": "node ../../../scripts/publish/prepare-publish.mjs postpack"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@volter/world-core": "2.0.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/bun": "^1.2.20",
|
|
43
|
+
"@types/node": "^24.0.0",
|
|
44
|
+
"@types/react": "^19.2.17",
|
|
45
|
+
"@types/react-dom": "^19.2.3",
|
|
46
|
+
"@volter/world-core": "2.0.0",
|
|
47
|
+
"@volter/world-tooling": "0.1.0",
|
|
48
|
+
"linkedin-api-client": "^0.3.0",
|
|
49
|
+
"typescript": "^5.9.0"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=22.3"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"react": "^19.2.7",
|
|
56
|
+
"react-dom": "^19.2.7"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { keepProcessAlive } from '@volter/world-core/lifecycle';
|
|
3
|
+
import { hasFlag, optionValue } from '@volter/world-core/args';
|
|
4
|
+
import { createLinkedinTwinServer } from './linkedin-server.ts';
|
|
5
|
+
import { createLinkedinMirrorServer } from './linkedin-mirror-ui.ts';
|
|
6
|
+
|
|
7
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
8
|
+
const port = Number(optionValue(rest, '--port', '0')) || undefined;
|
|
9
|
+
const root = optionValue(rest, '--root') || undefined;
|
|
10
|
+
const readOnly = hasFlag(rest, '--read-only');
|
|
11
|
+
|
|
12
|
+
if (cmd === 'serve') {
|
|
13
|
+
const server = await createLinkedinTwinServer({ readOnly, ...(root ? { root } : {}), ...(port ? { port } : {}) });
|
|
14
|
+
process.stdout.write(`linkedin twin${readOnly ? ' [read-only]' : ''} at http://127.0.0.1:${server.port}\n`);
|
|
15
|
+
await keepProcessAlive();
|
|
16
|
+
} else if (cmd === 'mirror') {
|
|
17
|
+
const s = await createLinkedinMirrorServer({ readOnly, ...(root ? { root } : {}), ...(port ? { port } : {}) });
|
|
18
|
+
process.stdout.write(`linkedin mirror UI (linkedin.com: sign in, company page, posts, single post) at ${s.url}\n`);
|
|
19
|
+
await keepProcessAlive();
|
|
20
|
+
} else if (cmd === 'conformance') {
|
|
21
|
+
// Lazy — the conformance module is DEV-ONLY and must not be reachable from the runtime entrypoints.
|
|
22
|
+
const { checkLinkedinConformance } = await import('./linkedin-conformance.ts');
|
|
23
|
+
const report = await checkLinkedinConformance({ ...(root ? { root } : {}) });
|
|
24
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
25
|
+
if (!report.ok) process.exitCode = 1;
|
|
26
|
+
} else {
|
|
27
|
+
process.stdout.write('Usage: world-linkedin serve|mirror|conformance [--port N] [--root DIR] [--read-only]\n');
|
|
28
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export { ACTIVE_VERSIONS, handleLinkedinTwinRequest, projectPost, templateHashtags } from './linkedin-twin.ts';
|
|
2
|
+
export type { LinkedinRequest } from './linkedin-twin.ts';
|
|
3
|
+
export type { LinkedinResponse } from './linkedin-errors.ts';
|
|
4
|
+
export { createLinkedinTwinFetch, createLinkedinTwinServer, type LinkedinTwinFetchOptions } from './linkedin-server.ts';
|
|
5
|
+
export {
|
|
6
|
+
budgetedLinkedinExecute,
|
|
7
|
+
LINKEDIN_VERSION,
|
|
8
|
+
MAX_PROCESSING_WAIT_MS,
|
|
9
|
+
performLinkedinAction,
|
|
10
|
+
performLinkedinActionWithin,
|
|
11
|
+
postBodyForVendor,
|
|
12
|
+
syncLinkedinFromReal,
|
|
13
|
+
syncLinkedinFromRemote,
|
|
14
|
+
untemplateHashtags,
|
|
15
|
+
uploadVideoToVendor,
|
|
16
|
+
} from './linkedin-connector.ts';
|
|
17
|
+
export {
|
|
18
|
+
buildLinkedinMirrorClient,
|
|
19
|
+
commentarySegments,
|
|
20
|
+
createLinkedinMirrorServer,
|
|
21
|
+
followerLine,
|
|
22
|
+
linkedinMirrorHtml,
|
|
23
|
+
linkedinMirrorStyles,
|
|
24
|
+
mediaKind,
|
|
25
|
+
MIRROR_VERSION,
|
|
26
|
+
needsFold,
|
|
27
|
+
postRoute,
|
|
28
|
+
postsQuery,
|
|
29
|
+
relativeTime,
|
|
30
|
+
sourceHost,
|
|
31
|
+
} from './linkedin-mirror-ui.ts';
|
|
32
|
+
export type { LiRow, Segment } from './linkedin-mirror-ui.ts';
|
|
33
|
+
// The client-side rate budget the perform and refresh adapters charge (linkedin-budget.ts says how the
|
|
34
|
+
// numbers were chosen). The mechanism is the kernel's; these are this vendor's numbers.
|
|
35
|
+
export {
|
|
36
|
+
LINKEDIN_BUDGET_BURST_CEILING,
|
|
37
|
+
LINKEDIN_BUDGET_CEILING,
|
|
38
|
+
LINKEDIN_BUDGET_MAX_RETRY_AFTER_S,
|
|
39
|
+
LINKEDIN_BUDGET_WINDOW_MS,
|
|
40
|
+
LINKEDIN_CALL_WEIGHTS,
|
|
41
|
+
LINKEDIN_RATE_BUDGET,
|
|
42
|
+
LinkedinBudget,
|
|
43
|
+
LinkedinBudgetError,
|
|
44
|
+
linkedinBudgetPath,
|
|
45
|
+
linkedinCallWeight,
|
|
46
|
+
} from './linkedin-budget.ts';
|
|
47
|
+
|
|
48
|
+
import { registerPack, type TwinPack } from '@volter/world-core';
|
|
49
|
+
import { performLinkedinAction, syncLinkedinFromRemote } from './linkedin-connector.ts';
|
|
50
|
+
import { LINKEDIN_RATE_BUDGET as RATE_BUDGET } from './linkedin-budget.ts';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The api.linkedin.com paths THIS pack serves, as a RegExp SOURCE for the descriptor's `hosts` path
|
|
54
|
+
* rule: the versioned `/rest` resources an organization posts through, the OpenID userinfo, and the
|
|
55
|
+
* twin-only control prefix. ANCHORED, so `/rest/postsomething` stays unclaimed; everything else on
|
|
56
|
+
* api.linkedin.com (ads, analytics, comments, reactions, the legacy /v2 resources) refuses loudly at
|
|
57
|
+
* the real vendor rather than landing in a twin that cannot serve it.
|
|
58
|
+
*/
|
|
59
|
+
const API_PATHS = '^(?:/rest/(?:posts|images|videos|organizationAcls|organizations|organizationsLookup|networkSizes)(?:/[^/]+)?/?|/v2/userinfo/?)$|^/_twin/';
|
|
60
|
+
|
|
61
|
+
export const pack: TwinPack = {
|
|
62
|
+
vendor: 'linkedin',
|
|
63
|
+
// The SAME object linkedin-budget.ts declares at module load — one source of truth.
|
|
64
|
+
rateBudget: RATE_BUDGET,
|
|
65
|
+
transport: 'rest',
|
|
66
|
+
// PROTOCOL 2 (docs/contributing/architecture.md#protocol-2-the-pack-is-a-plugin): the pack is a
|
|
67
|
+
// plugin — its wire, its tree, and its half of the real state system. Born on it.
|
|
68
|
+
protocol: '2',
|
|
69
|
+
archetype: 'crud',
|
|
70
|
+
bin: 'world-linkedin',
|
|
71
|
+
resources: ['organization', 'member', 'organization_acl', 'token', 'post'],
|
|
72
|
+
specSource: 'LinkedIn Marketing API docs (learn.microsoft.com/linkedin, li-lms-2026-09), read 2026-09-26: Posts API + Post Schema, Images API, Videos API, Organization Access Control by Role, Organization Lookup (incl. networkSizes), Versioning, Error Responses, Rate Limiting; and Sign In with LinkedIn using OpenID Connect (/v2/userinfo). LinkedIn publishes no machine-readable spec for this surface.',
|
|
73
|
+
description: "LinkedIn twin — an organization's posts (commentary with one image, one video or an article) through the versioned Posts API, the Images and Videos upload protocols with signed upload URLs, the page's admin roles and follower count, and a linkedin.com company-page mirror; everything else refuses by name.",
|
|
74
|
+
// A page's posts barely move and LinkedIn's limits are daily: hourly, and on demand.
|
|
75
|
+
refresh: { every: '1h', webhook: false, onDemand: { atMost: '60s' } },
|
|
76
|
+
stateSystem: { perform: performLinkedinAction, refresh: syncLinkedinFromRemote },
|
|
77
|
+
// The round trip: the page, its admin, the admin's role and the token the OAuth leg would have
|
|
78
|
+
// minted (the vendor has no create-from-nothing for any of them), then a post — minted fresh from
|
|
79
|
+
// the tree on every send.
|
|
80
|
+
roundTrip: [
|
|
81
|
+
{ method: 'POST', path: '/_twin/organizations', body: { id: '2414183', localizedName: 'Round Trip Co', vanityName: 'round-trip-co' } },
|
|
82
|
+
{ method: 'POST', path: '/_twin/members', body: { id: 'roundTrip', localizedFirstName: 'Round', localizedLastName: 'Trip', email: 'round.trip@example.com' } },
|
|
83
|
+
{ method: 'POST', path: '/_twin/organizationAcls', body: { organization: 'urn:li:organization:2414183', roleAssignee: 'urn:li:person:roundTrip', role: 'ADMINISTRATOR', state: 'APPROVED' } },
|
|
84
|
+
{ method: 'POST', path: '/_twin/tokens', body: { token: 'round-trip-token', member: 'urn:li:person:roundTrip', scopes: ['w_organization_social', 'r_organization_social', 'rw_organization_admin'] } },
|
|
85
|
+
{
|
|
86
|
+
method: 'POST', path: '/rest/posts',
|
|
87
|
+
headers: { authorization: 'Bearer round-trip-token', 'linkedin-version': '202609', 'x-restli-protocol-version': '2.0.0' },
|
|
88
|
+
body: { author: 'urn:li:organization:2414183', commentary: 'round trip', visibility: 'PUBLIC', distribution: { feedDistribution: 'MAIN_FEED', targetEntities: [], thirdPartyDistributionChannels: [] }, lifecycleState: 'PUBLISHED', isReshareDisabledByAuthor: false },
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
parityOrigin: 'http://twin',
|
|
92
|
+
// the write handler and the refresh adapter store the same shape (scripts/shape-parity.test.ts)
|
|
93
|
+
shapeParity: 'held',
|
|
94
|
+
|
|
95
|
+
// ADOPTION. `linkedin-api-client` is LinkedIn's own JavaScript client ("Official JavaScript client
|
|
96
|
+
// library for LinkedIn APIs", github.com/linkedin-developers/linkedin-api-js-client), and the
|
|
97
|
+
// Python client of the same repo family is `linkedin-api-client` on PyPI. LINKEDIN is the stem of
|
|
98
|
+
// LINKEDIN_ACCESS_TOKEN / LINKEDIN_CLIENT_ID et al.
|
|
99
|
+
adoption: {
|
|
100
|
+
sdks: ['linkedin-api-client'],
|
|
101
|
+
pypi: ['linkedin-api-client'],
|
|
102
|
+
envStems: ['LINKEDIN'],
|
|
103
|
+
},
|
|
104
|
+
// INTERCEPTION. api.linkedin.com for the API (path-scoped: this pack models one slice of it);
|
|
105
|
+
// www.linkedin.com only for the signed upload URLs (`/dms-uploads/`) the Images and Videos APIs
|
|
106
|
+
// return — never the site itself; media.licdn.com and dms.licdn.com for the image and video bytes
|
|
107
|
+
// an asset's downloadUrl names.
|
|
108
|
+
hosts: [
|
|
109
|
+
{ host: 'api.linkedin.com', pathPattern: API_PATHS },
|
|
110
|
+
{ host: 'www.linkedin.com', pathPattern: '^/dms-uploads/' },
|
|
111
|
+
{ host: 'media.licdn.com', pathPattern: '^/dms/image/' },
|
|
112
|
+
{ host: 'dms.licdn.com', pathPattern: '^/playlist/' },
|
|
113
|
+
],
|
|
114
|
+
endpointEnvNone: "linkedin-api-client takes no base-URL environment variable (its RestliClient builds https://api.linkedin.com/rest URLs from a constant), so a World reaches it through the hosts above; inventing a LINKEDIN_BASE_URL the app never reads would report coverage the app does not have.",
|
|
115
|
+
browserRouting: { apiPathPrefix: '/rest/', loaderHost: 'https://api.linkedin.com' },
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
registerPack(pack);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// LinkedIn's CLIENT-SIDE RATE BUDGET — this pack's DECLARATION (the numbers) plus the thin typed
|
|
2
|
+
// bindings the perform and refresh adapters use. The MECHANISM — the durable token-keyed ledger, the
|
|
3
|
+
// rolling window, reserve-under-lock, the Retry-After / 429 cooldown, fail-CLOSED on a corrupt ledger
|
|
4
|
+
// — lives ONCE in the kernel (`@volter/world-core` → rateBudget.ts).
|
|
5
|
+
//
|
|
6
|
+
// ── HOW THE CEILING WAS CHOSEN ──────────────────────────────────────────────────────────────
|
|
7
|
+
// LinkedIn's published scheme, live-fetched for this build (learn.microsoft.com/linkedin/shared/
|
|
8
|
+
// api-guide/concepts/rate-limits, fetched 2026-09-26): "Rate limits specify the maximum number of
|
|
9
|
+
// API calls that can be made in a 24 hour period. These limits reset at midnight UTC every day",
|
|
10
|
+
// per application and per member, "Rate limited requests will receive a 429 response", and
|
|
11
|
+
// "Standard rate limits are not published in documentation" — each app reads its own in the
|
|
12
|
+
// Developer Portal. So there is NO first-party figure to transcribe, and the declaration stays at
|
|
13
|
+
// or under the kernel fallback: a one-hour window (the longest the kernel's rolling ledger keeps),
|
|
14
|
+
// 600 weighted units in it, and a burst of 30 units a minute — the fallback's own burst, so no
|
|
15
|
+
// VENDOR_BURST_ANCHOR is needed. A post or a delete (the org speaking in public) costs 5, the
|
|
16
|
+
// upload control calls 2, every read 1, anything unnamed 2.
|
|
17
|
+
//
|
|
18
|
+
// WHAT IS NOT CHARGED. The part uploads a video perform sends to the signed www.linkedin.com
|
|
19
|
+
// /dms-uploads URLs (presigned requests) are not API calls — the rate-limit page limits "API
|
|
20
|
+
// requests", and those URLs are the upload service, carrying their own signature — so the budgeted
|
|
21
|
+
// executor passes them through uncharged. Their number is bounded by construction instead: a
|
|
22
|
+
// perform refuses a video over the feed's 500 MB (at most 125 parts of 4 MB) before initializing.
|
|
23
|
+
import {
|
|
24
|
+
declareRateBudget,
|
|
25
|
+
rateBudgetPath,
|
|
26
|
+
rateBudgetWeight,
|
|
27
|
+
RateBudget,
|
|
28
|
+
type RateBudgetDeclaration,
|
|
29
|
+
type RateBudgetOptions,
|
|
30
|
+
type RateBudgetReservation,
|
|
31
|
+
type RateBudgetSnapshot,
|
|
32
|
+
} from '@volter/world-core';
|
|
33
|
+
|
|
34
|
+
const VENDOR = 'linkedin';
|
|
35
|
+
|
|
36
|
+
/** Rolling window, in ms — the longest the kernel's ledger keeps (LinkedIn's own window is a day). */
|
|
37
|
+
export const LINKEDIN_BUDGET_WINDOW_MS = 60 * 60_000;
|
|
38
|
+
/** Weighted units allowed inside one window: conservative, not transcribed (no figure is published). */
|
|
39
|
+
export const LINKEDIN_BUDGET_CEILING = 600;
|
|
40
|
+
/** Weighted units in any 60 s: the kernel fallback's 30, so no burst anchor is required. */
|
|
41
|
+
export const LINKEDIN_BUDGET_BURST_CEILING = 30;
|
|
42
|
+
/** Seconds. A Retry-After above this means the member or app is out for the day — fail loudly. */
|
|
43
|
+
export const LINKEDIN_BUDGET_MAX_RETRY_AFTER_S = 3600;
|
|
44
|
+
|
|
45
|
+
export const LINKEDIN_CALL_WEIGHTS = {
|
|
46
|
+
/** A post or its deletion — the organization speaking, in public. */
|
|
47
|
+
write: 5,
|
|
48
|
+
/** initializeUpload / finalizeUpload. */
|
|
49
|
+
upload: 2,
|
|
50
|
+
/** A read: a video's status, a finder page, an organization, the ACLs. */
|
|
51
|
+
read: 1,
|
|
52
|
+
/** Everything else on the vendor: priced above a read. */
|
|
53
|
+
other: 2,
|
|
54
|
+
} as const;
|
|
55
|
+
|
|
56
|
+
/** THE PACK'S DECLARATION — pure data, the only LinkedIn-specific thing in the whole budget. */
|
|
57
|
+
export const LINKEDIN_RATE_BUDGET: RateBudgetDeclaration = {
|
|
58
|
+
windowMs: LINKEDIN_BUDGET_WINDOW_MS,
|
|
59
|
+
ceiling: LINKEDIN_BUDGET_CEILING,
|
|
60
|
+
burstCeiling: LINKEDIN_BUDGET_BURST_CEILING,
|
|
61
|
+
defaultWeight: LINKEDIN_CALL_WEIGHTS.other,
|
|
62
|
+
maxRetryAfterSeconds: LINKEDIN_BUDGET_MAX_RETRY_AFTER_S,
|
|
63
|
+
rules: [
|
|
64
|
+
{ match: '^POST /rest/posts$', weight: LINKEDIN_CALL_WEIGHTS.write },
|
|
65
|
+
{ match: '^DELETE /rest/posts/', weight: LINKEDIN_CALL_WEIGHTS.write },
|
|
66
|
+
{ match: '^POST /rest/(videos|images)$', weight: LINKEDIN_CALL_WEIGHTS.upload },
|
|
67
|
+
{ match: '^GET /rest/', weight: LINKEDIN_CALL_WEIGHTS.read },
|
|
68
|
+
],
|
|
69
|
+
reason:
|
|
70
|
+
'LinkedIn limits API calls per application and per member over a 24-hour period reset at midnight UTC, '
|
|
71
|
+
+ 'answers an exceeded limit with 429, and states "Standard rate limits are not published in documentation" '
|
|
72
|
+
+ '(learn.microsoft.com/linkedin/shared/api-guide/concepts/rate-limits, fetched 2026-09-26). With no published '
|
|
73
|
+
+ 'figure, this declaration stays at or under the kernel fallback rather than dressing a guess as a vendor fact: '
|
|
74
|
+
+ '600 weighted units per rolling hour (the longest window the ledger keeps) and a 30-unit burst per minute (the '
|
|
75
|
+
+ "fallback's own). A post or a delete costs 5, an upload initialize/finalize 2, a read 1, anything unnamed 2. The "
|
|
76
|
+
+ 'signed /dms-uploads part PUTs are not API requests and are not charged; a perform bounds them to 125 parts.',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
declareRateBudget(VENDOR, LINKEDIN_RATE_BUDGET);
|
|
80
|
+
|
|
81
|
+
/** Price one call, keyed `"<METHOD> <path>"` with the query split off and the method upper-cased. */
|
|
82
|
+
export function linkedinCallWeight(method: string, path: string): number {
|
|
83
|
+
const at = path.indexOf('?');
|
|
84
|
+
const query: Record<string, string> = {};
|
|
85
|
+
if (at !== -1) for (const [k, v] of new URLSearchParams(path.slice(at + 1))) query[k] = v;
|
|
86
|
+
const raw = at === -1 ? path : path.slice(0, at);
|
|
87
|
+
const bare = raw.length > 1 && raw.endsWith('/') ? raw.replace(/\/+$/, '') : raw;
|
|
88
|
+
return rateBudgetWeight(VENDOR, `${String(method).toUpperCase()} ${bare}`, query);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Where this vendor's ledger lives: token-keyed and cwd-independent by default. */
|
|
92
|
+
export function linkedinBudgetPath(opts: { root?: string; token?: string } | string = {}): string {
|
|
93
|
+
const o = typeof opts === 'string' ? { root: opts } : opts;
|
|
94
|
+
return rateBudgetPath({ ...o, vendor: VENDOR });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type LinkedinBudgetOptions = Omit<RateBudgetOptions, 'vendor'>;
|
|
98
|
+
|
|
99
|
+
/** This vendor's budget — the kernel guard bound to this declaration. */
|
|
100
|
+
export class LinkedinBudget extends RateBudget {
|
|
101
|
+
constructor(opts: LinkedinBudgetOptions = {}) {
|
|
102
|
+
super({ ...opts, vendor: VENDOR });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export { RateBudgetError as LinkedinBudgetError } from '@volter/world-core';
|
|
107
|
+
export type LinkedinBudgetReservation = RateBudgetReservation;
|
|
108
|
+
export type LinkedinBudgetSnapshot = RateBudgetSnapshot;
|