@singi-labs/sifa-sdk 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.
@@ -0,0 +1,495 @@
1
+ interface LocationValue {
2
+ city?: string;
3
+ region?: string;
4
+ country: string;
5
+ countryCode?: string;
6
+ postalCode?: string;
7
+ geonameId?: number;
8
+ }
9
+ interface SkillRef {
10
+ uri: string;
11
+ }
12
+ interface ProfilePosition {
13
+ rkey: string;
14
+ company: string;
15
+ title: string;
16
+ description?: string;
17
+ startedAt: string;
18
+ endedAt?: string;
19
+ location?: LocationValue | null;
20
+ workplaceType?: string;
21
+ skills?: SkillRef[];
22
+ linkedSkills?: ProfileSkill[];
23
+ primary?: boolean;
24
+ }
25
+ interface ProfileEducation {
26
+ rkey: string;
27
+ institution: string;
28
+ degree?: string;
29
+ fieldOfStudy?: string;
30
+ description?: string;
31
+ activities?: string;
32
+ startedAt?: string;
33
+ endedAt?: string;
34
+ }
35
+ interface ProfileSkill {
36
+ rkey: string;
37
+ name: string;
38
+ category?: string;
39
+ endorsementCount?: number;
40
+ endorsed?: boolean;
41
+ activityBacked?: boolean;
42
+ }
43
+ interface SkillSuggestion {
44
+ canonicalName: string;
45
+ slug: string;
46
+ category: string;
47
+ }
48
+ interface Endorsement {
49
+ endorserDid: string;
50
+ endorserHandle: string;
51
+ endorserDisplayName?: string;
52
+ endorserAvatar?: string;
53
+ comment?: string;
54
+ relationshipContext?: string;
55
+ createdAt: string;
56
+ }
57
+ interface EndorsementData {
58
+ skillRkey: string;
59
+ comment?: string;
60
+ relationshipContext?: string;
61
+ }
62
+ interface ProfileLocation {
63
+ rkey: string;
64
+ type: string;
65
+ label?: string | null;
66
+ isPrimary: boolean;
67
+ locationCountry?: string | null;
68
+ locationRegion?: string | null;
69
+ locationCity?: string | null;
70
+ countryCode?: string | null;
71
+ location?: string | null;
72
+ }
73
+ interface ProfileCertification {
74
+ rkey: string;
75
+ name: string;
76
+ issuingOrg: string;
77
+ issueDate?: string;
78
+ expiryDate?: string;
79
+ credentialUrl?: string;
80
+ }
81
+ interface ProfileProject {
82
+ rkey: string;
83
+ name: string;
84
+ description?: string;
85
+ url?: string;
86
+ startDate?: string;
87
+ endDate?: string;
88
+ }
89
+ interface PublicationContributor {
90
+ name: string;
91
+ orcidId?: string;
92
+ did?: string;
93
+ handle?: string;
94
+ }
95
+ interface ProfilePublication {
96
+ rkey: string;
97
+ title: string;
98
+ publisher?: string;
99
+ date?: string;
100
+ url?: string;
101
+ description?: string;
102
+ source?: 'sifa' | 'standard' | 'orcid';
103
+ doi?: string;
104
+ type?: string;
105
+ typeLabel?: string;
106
+ contributors?: PublicationContributor[];
107
+ verified?: boolean;
108
+ verifiedVia?: string;
109
+ orcidPutCode?: number;
110
+ hidden?: boolean;
111
+ orcidCorroborated?: boolean;
112
+ pendingVerification?: boolean;
113
+ appId?: string;
114
+ }
115
+ interface ProfileVolunteering {
116
+ rkey: string;
117
+ organization: string;
118
+ role?: string;
119
+ cause?: string;
120
+ startDate?: string;
121
+ endDate?: string;
122
+ description?: string;
123
+ }
124
+ interface ProfileHonor {
125
+ rkey: string;
126
+ title: string;
127
+ issuer?: string;
128
+ date?: string;
129
+ description?: string;
130
+ }
131
+ type LanguageProficiency = 'elementary' | 'limited_working' | 'professional_working' | 'full_professional' | 'native';
132
+ interface ProfileLanguage {
133
+ rkey: string;
134
+ language: string;
135
+ proficiency?: LanguageProficiency;
136
+ }
137
+ interface ProfileCourse {
138
+ rkey: string;
139
+ name: string;
140
+ institution?: string;
141
+ number?: string;
142
+ }
143
+ interface TrustStat {
144
+ key: string;
145
+ label: string;
146
+ value: number;
147
+ }
148
+ interface ActiveApp {
149
+ id: string;
150
+ name: string;
151
+ category: string;
152
+ recentCount: number;
153
+ latestRecordAt?: string | null;
154
+ }
155
+ interface VerifiedAccount {
156
+ platform: string;
157
+ identifier: string;
158
+ url?: string;
159
+ }
160
+ interface ExternalAccountKeytraceClaim {
161
+ rkey: string;
162
+ claimedAt: string;
163
+ }
164
+ interface ExternalAccount {
165
+ rkey: string;
166
+ platform: string;
167
+ url: string;
168
+ label?: string;
169
+ feedUrl?: string;
170
+ primary?: boolean;
171
+ verifiable: boolean;
172
+ verified: boolean;
173
+ verifiedVia?: string | null;
174
+ source?: 'sifa' | 'keytrace' | 'keyoxide';
175
+ hidden?: boolean;
176
+ keytraceVerified?: boolean;
177
+ keytraceClaim?: ExternalAccountKeytraceClaim;
178
+ }
179
+ interface FeedItem {
180
+ title: string;
181
+ excerpt: string;
182
+ url: string;
183
+ timestamp: string;
184
+ source: string;
185
+ }
186
+ interface PdsProviderInfo {
187
+ name: string;
188
+ host: string;
189
+ }
190
+ interface ProfileIndustry {
191
+ industry: string;
192
+ domain?: string;
193
+ }
194
+ interface ProfileOverrideSource {
195
+ headline?: string;
196
+ about?: string;
197
+ displayName?: string;
198
+ avatarUrl?: string;
199
+ }
200
+ interface Profile {
201
+ did: string;
202
+ handle: string;
203
+ displayName?: string;
204
+ avatar?: string;
205
+ pronouns?: string;
206
+ headline?: string;
207
+ about?: string;
208
+ hasHeadlineOverride?: boolean;
209
+ hasAboutOverride?: boolean;
210
+ hasDisplayNameOverride?: boolean;
211
+ hasAvatarUrlOverride?: boolean;
212
+ source?: ProfileOverrideSource;
213
+ industries?: ProfileIndustry[];
214
+ location?: LocationValue | null;
215
+ locations?: ProfileLocation[];
216
+ website?: string;
217
+ openTo?: string[];
218
+ preferredWorkplace?: string[];
219
+ availableFromUtc?: number;
220
+ availableToUtc?: number;
221
+ pdsProvider?: PdsProviderInfo | null;
222
+ claimed: boolean;
223
+ isOwnProfile?: boolean;
224
+ createdAt?: string;
225
+ trustStats?: TrustStat[];
226
+ verifiedAccounts?: VerifiedAccount[];
227
+ activeApps?: ActiveApp[];
228
+ blueskyVerified?: boolean;
229
+ blueskyVerifiedAt?: string | null;
230
+ followersCount: number;
231
+ followingCount: number;
232
+ connectionsCount: number;
233
+ atprotoFollowersCount?: number;
234
+ positions: ProfilePosition[];
235
+ education: ProfileEducation[];
236
+ skills: ProfileSkill[];
237
+ certifications?: ProfileCertification[];
238
+ projects?: ProfileProject[];
239
+ publications?: ProfilePublication[];
240
+ volunteering?: ProfileVolunteering[];
241
+ honors?: ProfileHonor[];
242
+ languages?: ProfileLanguage[];
243
+ courses?: ProfileCourse[];
244
+ externalAccounts?: ExternalAccount[];
245
+ }
246
+
247
+ type ContinentCode = 'AF' | 'AN' | 'AS' | 'EU' | 'NA' | 'OC' | 'SA';
248
+ declare const CONTINENTS: ReadonlyArray<{
249
+ code: ContinentCode;
250
+ name: string;
251
+ }>;
252
+ declare function getContinent(countryCode: string): ContinentCode | null;
253
+
254
+ /** ISO 3166-1 country list for local fallback when GeoNames is unavailable. */
255
+ declare const COUNTRIES: ReadonlyArray<{
256
+ code: string;
257
+ name: string;
258
+ }>;
259
+
260
+ /** Two-level industry/domain taxonomy for profile classification. */
261
+ interface IndustryOption {
262
+ value: string;
263
+ labelKey: string;
264
+ domains: {
265
+ value: string;
266
+ labelKey: string;
267
+ }[];
268
+ }
269
+ declare const INDUSTRY_OPTIONS: IndustryOption[];
270
+
271
+ /**
272
+ * Platform identifiers, labels, and URL helpers used in profile external accounts.
273
+ *
274
+ * This file is intentionally data-only -- icon mapping is platform-specific
275
+ * (React DOM components on web, React Native components on mobile) and lives
276
+ * in the consumer.
277
+ */
278
+ declare const PLATFORM_LABELS: {
279
+ readonly bluesky: "Bluesky";
280
+ readonly github: "GitHub";
281
+ readonly linkedin: "LinkedIn";
282
+ readonly youtube: "YouTube";
283
+ readonly twitter: "X (Twitter)";
284
+ readonly instagram: "Instagram";
285
+ readonly substack: "Substack";
286
+ readonly tangled: "Tangled";
287
+ readonly dns: "Domain";
288
+ readonly website: "Website";
289
+ readonly rss: "RSS";
290
+ readonly fediverse: "Fediverse";
291
+ readonly orcid: "ORCID";
292
+ readonly keyoxide: "Keyoxide";
293
+ };
294
+ type PlatformId = keyof typeof PLATFORM_LABELS;
295
+ declare function isKnownPlatform(platform: string): platform is PlatformId;
296
+ declare function getPlatformLabel(platform: string): string;
297
+ /** Platforms available in the "Add Links" dropdown. */
298
+ declare const PLATFORM_OPTIONS: ReadonlyArray<{
299
+ value: PlatformId;
300
+ label: string;
301
+ }>;
302
+ /**
303
+ * Build a favicon URL for a given site URL using Google's public favicon service.
304
+ * Returns `null` if the URL cannot be parsed.
305
+ */
306
+ declare function getFaviconUrl(siteUrl: string): string | null;
307
+
308
+ declare const SKILL_CATEGORIES: readonly [{
309
+ readonly value: "technical";
310
+ readonly label: "Technical";
311
+ }, {
312
+ readonly value: "business";
313
+ readonly label: "Business";
314
+ }, {
315
+ readonly value: "creative";
316
+ readonly label: "Creative";
317
+ }, {
318
+ readonly value: "interpersonal";
319
+ readonly label: "Interpersonal";
320
+ }, {
321
+ readonly value: "industry";
322
+ readonly label: "Industry";
323
+ }, {
324
+ readonly value: "community";
325
+ readonly label: "Community";
326
+ }, {
327
+ readonly value: "security";
328
+ readonly label: "Security";
329
+ }];
330
+ type SkillCategory = (typeof SKILL_CATEGORIES)[number]['value'];
331
+ declare const CATEGORY_ORDER: ReadonlyArray<SkillCategory>;
332
+ declare const CATEGORY_LABELS: Record<SkillCategory | 'other', string>;
333
+
334
+ type MergedProfileSkill = ProfileSkill & {
335
+ mergedRkeys: string[];
336
+ positionRkeys?: string[];
337
+ };
338
+ /**
339
+ * Collapses skills that resolve to the same name (case-insensitive, trimmed).
340
+ *
341
+ * LinkedIn import creates one skill record per (skill, position) pair, so a
342
+ * profile can have several "Ruby" rows. We surface them as a single chip with
343
+ * all underlying rkeys exposed via `mergedRkeys` so edit/delete can fan out.
344
+ */
345
+ declare function dedupeSkills(skills: ProfileSkill[]): MergedProfileSkill[];
346
+ /**
347
+ * Groups skills by category in CATEGORY_ORDER, with unknown/empty categories
348
+ * collected under "other". Within each group: sorted by endorsementCount desc,
349
+ * then alphabetical by name. Empty groups are omitted.
350
+ */
351
+ declare function groupSkillsByCategory<T extends ProfileSkill>(skills: T[]): [string, T[]][];
352
+
353
+ /**
354
+ * Format a date string as a relative time (e.g. "5m ago", "3d ago").
355
+ * Returns an empty string for invalid or future dates.
356
+ */
357
+ declare function formatRelativeTime(dateString: string): string;
358
+
359
+ /**
360
+ * Sort profile section items from newest to oldest.
361
+ *
362
+ * Rules:
363
+ * - Items with no end date (ongoing/current) sort to the top.
364
+ * - Primary sort: end date descending (newest first).
365
+ * - Secondary sort: start date descending (newest first).
366
+ * - Items with no dates sort to the bottom.
367
+ *
368
+ * Date strings are ISO-ish ("2024", "2024-03", "2024-03-15") and
369
+ * lexicographic comparison works correctly for them.
370
+ */
371
+ interface DateRange {
372
+ startDate?: string;
373
+ endDate?: string;
374
+ current?: boolean;
375
+ }
376
+ type DateExtractor<T> = (item: T) => DateRange;
377
+ declare function sortByDateDesc<T>(items: T[], extract: DateExtractor<T>): T[];
378
+ /**
379
+ * Convenience extractor for items that use `startDate` / `endDate` fields directly.
380
+ * Used by volunteering, projects, and other sections that still use startDate/endDate.
381
+ */
382
+ declare function dateRangeExtractor<T extends {
383
+ startDate?: string;
384
+ endDate?: string;
385
+ current?: boolean;
386
+ }>(item: T): DateRange;
387
+ /**
388
+ * Extractor for lexicon-aligned items that use `startedAt` / `endedAt` fields.
389
+ * Used by positions and education (current derived from !endedAt).
390
+ */
391
+ declare function lexiconDateExtractor<T extends {
392
+ startedAt?: string;
393
+ endedAt?: string;
394
+ }>(item: T): DateRange;
395
+ /**
396
+ * Convenience extractor for items with a single `date` field.
397
+ */
398
+ declare function singleDateExtractor<T extends {
399
+ date?: string;
400
+ }>(item: T): DateRange;
401
+ /**
402
+ * Extractor for certifications which use `issueDate` / `expiryDate`.
403
+ */
404
+ declare function certDateExtractor<T extends {
405
+ issueDate?: string;
406
+ expiryDate?: string;
407
+ }>(item: T): DateRange;
408
+
409
+ /**
410
+ * Sanitize raw user input into a valid AT Protocol identifier (handle or DID).
411
+ *
412
+ * Handles common mistakes:
413
+ * - Pasting a full Bluesky profile URL
414
+ * - Pasting a raw HTTPS URL to a PDS domain
415
+ * - Using an at:// URI
416
+ * - Prefixing with @
417
+ * - Entering a bare username without .bsky.social
418
+ */
419
+ declare function sanitizeHandleInput(raw: string): string;
420
+
421
+ /** Format a LocationValue for display */
422
+ declare function formatLocation(loc: LocationValue | null | undefined): string;
423
+ /** Parse a display string back into a LocationValue (best-effort for legacy data) */
424
+ declare function parseLocationString(str: string): LocationValue | null;
425
+ /** Convert ISO 3166-1 alpha-2 country code to flag emoji */
426
+ declare function countryCodeToFlag(code: string | undefined): string;
427
+
428
+ interface PdsProvider {
429
+ name: string;
430
+ profileUrl: string;
431
+ host?: string;
432
+ }
433
+ declare function pdsProviderFromApi(apiProvider: PdsProviderInfo | null | undefined, handle: string): PdsProvider | null;
434
+ declare function getHandleStem(handle: string): string;
435
+ declare function getDisplayLabel(displayName: string | undefined, handle: string): string;
436
+ declare function getPdsDisplayName(providerName: string): string;
437
+ declare function detectPdsProvider(handle: string): PdsProvider | null;
438
+
439
+ /**
440
+ * Truncate a string to at most `maxLen` grapheme clusters, appending an
441
+ * ellipsis when the string was shortened. Grapheme-aware so it never splits
442
+ * emoji sequences (ZWJ, regional indicators, combining marks, surrogate pairs).
443
+ */
444
+ declare function truncateGraphemes(value: string, maxLen: number): string;
445
+
446
+ /**
447
+ * Format a date as a relative time string (e.g. "2 minutes ago", "3 days ago").
448
+ */
449
+ declare function formatDistanceToNow(date: Date): string;
450
+
451
+ /**
452
+ * WCAG 2.2 contrast ratio utilities for validating publication theme colors.
453
+ * Implements the relative luminance and contrast ratio formulas from
454
+ * https://www.w3.org/TR/WCAG22/#dfn-relative-luminance
455
+ */
456
+ interface RgbColor {
457
+ r: number;
458
+ g: number;
459
+ b: number;
460
+ }
461
+ /**
462
+ * Return true when the value is an RgbColor with all channels as integers in [0, 255].
463
+ * Use this before passing untrusted data to the luminance / contrast functions.
464
+ */
465
+ declare function isValidRgbColor(value: unknown): value is RgbColor;
466
+ /**
467
+ * Serialize an RgbColor to a CSS `rgb(...)` string.
468
+ * Channels are floored to integers to prevent sub-pixel drift.
469
+ */
470
+ declare function rgbToString(color: RgbColor): string;
471
+ /**
472
+ * Calculate relative luminance of an sRGB color per WCAG 2.2.
473
+ * Input channels must be numbers in [0, 255].
474
+ */
475
+ declare function relativeLuminance(color: RgbColor): number;
476
+ /**
477
+ * Calculate WCAG 2.2 contrast ratio between two colors.
478
+ * Returns a value between 1 (identical) and 21 (black/white).
479
+ */
480
+ declare function contrastRatio(color1: RgbColor, color2: RgbColor): number;
481
+ /**
482
+ * Check if two colors meet WCAG 2.2 AA contrast requirement (4.5:1 for normal text).
483
+ */
484
+ declare function meetsContrastAA(foreground: RgbColor, background: RgbColor): boolean;
485
+
486
+ /**
487
+ * Sifa SDK -- public client library for the Sifa AppView on AT Protocol.
488
+ *
489
+ * Pre-1.0: the public API is unstable and may change in any minor release.
490
+ *
491
+ * @see https://github.com/singi-labs/sifa-sdk
492
+ */
493
+ declare const SIFA_SDK_VERSION: string;
494
+
495
+ export { type ActiveApp, CATEGORY_LABELS, CATEGORY_ORDER, CONTINENTS, COUNTRIES, type ContinentCode, type Endorsement, type EndorsementData, type ExternalAccount, type ExternalAccountKeytraceClaim, type FeedItem, INDUSTRY_OPTIONS, type IndustryOption, type LanguageProficiency, type LocationValue, type MergedProfileSkill, PLATFORM_LABELS, PLATFORM_OPTIONS, type PdsProvider, type PdsProviderInfo, type PlatformId, type Profile, type ProfileCertification, type ProfileCourse, type ProfileEducation, type ProfileHonor, type ProfileIndustry, type ProfileLanguage, type ProfileLocation, type ProfileOverrideSource, type ProfilePosition, type ProfileProject, type ProfilePublication, type ProfileSkill, type ProfileVolunteering, type PublicationContributor, type RgbColor, SIFA_SDK_VERSION, SKILL_CATEGORIES, type SkillCategory, type SkillRef, type SkillSuggestion, type TrustStat, type VerifiedAccount, certDateExtractor, contrastRatio, countryCodeToFlag, dateRangeExtractor, dedupeSkills, detectPdsProvider, formatDistanceToNow, formatLocation, formatRelativeTime, getContinent, getDisplayLabel, getFaviconUrl, getHandleStem, getPdsDisplayName, getPlatformLabel, groupSkillsByCategory, isKnownPlatform, isValidRgbColor, lexiconDateExtractor, meetsContrastAA, parseLocationString, pdsProviderFromApi, relativeLuminance, rgbToString, sanitizeHandleInput, singleDateExtractor, sortByDateDesc, truncateGraphemes };