@singi-labs/sifa-sdk 0.19.89 → 0.19.91

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.
@@ -1,7 +1,83 @@
1
- import { L as LocationValue, d as ProfilePosition, f as ProfileEducation, P as ProfileSkill, e as ProfileCertification, i as ProfileProject, j as ProfilePublication, N as ProfileVolunteering, g as ProfileHonor, h as ProfileLanguage, p as ExternalAccount, M as ProfilePresentation, B as ProfileCourse } from '../index-BEAuoL6l.js';
2
1
  import { z } from 'zod';
2
+ import { L as LocationValue, d as ProfilePosition, f as ProfileEducation, P as ProfileSkill, e as ProfileCertification, i as ProfileProject, j as ProfilePublication, N as ProfileVolunteering, g as ProfileHonor, h as ProfileLanguage, p as ExternalAccount, M as ProfilePresentation, B as ProfileCourse } from '../index-BEAuoL6l.js';
3
3
  import '../verification-providers-DeyGqaoC.js';
4
4
 
5
+ /**
6
+ * Private contact and personal details for exports: email, phone, postal
7
+ * address, driver's license, right to work and nationality.
8
+ *
9
+ * Sifa never stores these and never writes them to the owner's PDS, where they
10
+ * would be public. The owner types them on the export page, the browser keeps
11
+ * them, and they go only into the files the owner downloads. This module holds
12
+ * the shape, the limits and the pick rule, so the export page and the server
13
+ * that builds a funder CV agree on them.
14
+ */
15
+
16
+ /** Longest value accepted per field. 254 is the longest valid email address. */
17
+ declare const EXPORT_CONTACT_LIMITS: {
18
+ readonly email: 254;
19
+ readonly phone: 40;
20
+ readonly address: 500;
21
+ readonly driversLicense: 60;
22
+ readonly rightToWork: 200;
23
+ readonly nationality: 100;
24
+ };
25
+ /** The contact details that go into an export. Absent keys are left out. */
26
+ interface ExportContact {
27
+ readonly email?: string;
28
+ readonly phone?: string;
29
+ /** Free text; lines are separated by `\n`. */
30
+ readonly address?: string;
31
+ /** License categories, e.g. "B, BE". */
32
+ readonly driversLicense?: string;
33
+ /** e.g. "EU citizen, no sponsorship needed". */
34
+ readonly rightToWork?: string;
35
+ readonly nationality?: string;
36
+ }
37
+ /** What the owner has typed, plus which fields they chose to include. */
38
+ interface ExportContactDraft {
39
+ readonly email?: string;
40
+ readonly phone?: string;
41
+ readonly address?: string;
42
+ readonly driversLicense?: string;
43
+ readonly rightToWork?: string;
44
+ readonly nationality?: string;
45
+ readonly includeEmail?: boolean;
46
+ readonly includePhone?: boolean;
47
+ readonly includeAddress?: boolean;
48
+ readonly includeDriversLicense?: boolean;
49
+ readonly includeRightToWork?: boolean;
50
+ readonly includeNationality?: boolean;
51
+ }
52
+ /**
53
+ * Validates a picked contact. Only lengths are checked: phone numbers and
54
+ * addresses vary too much between countries, and a wrong-looking email is a
55
+ * warning for the owner, not a reason to refuse the export.
56
+ */
57
+ declare const exportContactSchema: z.ZodObject<{
58
+ email: z.ZodOptional<z.ZodString>;
59
+ phone: z.ZodOptional<z.ZodString>;
60
+ address: z.ZodOptional<z.ZodString>;
61
+ driversLicense: z.ZodOptional<z.ZodString>;
62
+ rightToWork: z.ZodOptional<z.ZodString>;
63
+ nationality: z.ZodOptional<z.ZodString>;
64
+ }, z.core.$strict>;
65
+ /**
66
+ * The fields to put in an export: the included ones that are not blank, run
67
+ * through {@link normalizeExportContact}.
68
+ */
69
+ declare function pickExportContact(draft: ExportContactDraft): ExportContact;
70
+ /**
71
+ * Drops blank fields and tidies the rest. Every field but the address becomes
72
+ * one line with single spaces. An address keeps its line breaks but loses blank lines and the
73
+ * spaces around each line. Each field is cut at its {@link EXPORT_CONTACT_LIMITS}
74
+ * length. The emitters call this too, so a caller that skips
75
+ * {@link pickExportContact} and the schema gets the same bounded output.
76
+ */
77
+ declare function normalizeExportContact(contact: ExportContact): ExportContact;
78
+ /** A loose shape check for a non-blocking "check this address" hint. */
79
+ declare function looksLikeEmail(value: string): boolean;
80
+
5
81
  /**
6
82
  * JSON Resume v1.0.0 emitter for a Sifa profile.
7
83
  *
@@ -33,6 +109,12 @@ interface JsonResumeOptions {
33
109
  * not at that path.
34
110
  */
35
111
  readonly canonicalUrl?: string;
112
+ /**
113
+ * Email, phone and postal address to print. Sifa holds none of these; the
114
+ * owner types them on the export page. Without it the resume has no
115
+ * `email`, `phone` or `location.address`.
116
+ */
117
+ readonly contact?: ExportContact;
36
118
  }
37
119
  /**
38
120
  * The subset of `Profile` this emitter reads. Deliberately structural rather
@@ -61,6 +143,8 @@ interface JsonResumeProfileInput {
61
143
  readonly externalAccounts?: ExternalAccount[];
62
144
  }
63
145
  interface JsonResumeLocation {
146
+ /** Street address; lines are separated by `\n`, as the schema asks. */
147
+ readonly address?: string;
64
148
  readonly city?: string;
65
149
  readonly region?: string;
66
150
  readonly countryCode?: string;
@@ -76,6 +160,15 @@ interface JsonResumeBasics {
76
160
  readonly label?: string;
77
161
  readonly image?: string;
78
162
  readonly url?: string;
163
+ readonly email?: string;
164
+ readonly phone?: string;
165
+ /**
166
+ * Not in the JSON Resume schema. Templates that do not know these keys
167
+ * ignore them; Sifa's own Typst CV prints them as personal details.
168
+ */
169
+ readonly driversLicense?: string;
170
+ readonly rightToWork?: string;
171
+ readonly nationality?: string;
79
172
  readonly summary?: string;
80
173
  readonly location?: JsonResumeLocation;
81
174
  readonly profiles?: JsonResumeProfileLink[];
@@ -297,6 +390,26 @@ interface FunderCvPerson {
297
390
  readonly orcid?: string;
298
391
  readonly website?: string;
299
392
  }
393
+ /**
394
+ * The ERC person, with the contact and personal details the owner typed for
395
+ * this export. The NIH Biographical Sketch has no such block, so only ERC carries them.
396
+ */
397
+ interface ErcFunderCvPerson extends FunderCvPerson {
398
+ readonly email?: string;
399
+ readonly phone?: string;
400
+ /** Lines are separated by `\n`. */
401
+ readonly address?: string;
402
+ readonly driversLicense?: string;
403
+ readonly rightToWork?: string;
404
+ readonly nationality?: string;
405
+ }
406
+ interface FunderCvOptions {
407
+ /**
408
+ * Contact details the owner typed for this export. Sifa stores none of
409
+ * them. Used for ERC only.
410
+ */
411
+ readonly contact?: ExportContact;
412
+ }
300
413
  /** One product or research output, ready to cite. */
301
414
  interface FunderCvCitation {
302
415
  readonly kind: 'publication' | 'presentation';
@@ -378,7 +491,7 @@ interface ErcContribution {
378
491
  /** ERC Part B1, section "Curriculum vitae and Track Record". */
379
492
  interface ErcFunderCv {
380
493
  readonly format: 'erc';
381
- readonly person: FunderCvPerson;
494
+ readonly person: ErcFunderCvPerson;
382
495
  readonly education: ErcEducation[];
383
496
  readonly currentPositions: FunderCvAppointment[];
384
497
  readonly previousPositions: FunderCvAppointment[];
@@ -398,6 +511,6 @@ type FunderCvDocument = NihFunderCv | ErcFunderCv;
398
511
  * skipped; callers that must refuse them check `findUnknownFunderCvItems`
399
512
  * first. Selected items keep the order they were picked in.
400
513
  */
401
- declare function profileToFunderCv(profile: FunderCvProfileInput, selection: FunderCvSelection): FunderCvDocument;
514
+ declare function profileToFunderCv(profile: FunderCvProfileInput, selection: FunderCvSelection, options?: FunderCvOptions): FunderCvDocument;
402
515
 
403
- export { type ErcContribution, type ErcEducation, type ErcFunderCv, type ErcRecognition, FUNDER_CV_FORMATS, FUNDER_CV_GROUPS, FUNDER_CV_ITEM_KINDS, type FunderCvAppointment, type FunderCvCandidate, type FunderCvCitation, type FunderCvDocument, type FunderCvFormat, type FunderCvGroupName, type FunderCvGroupSpec, type FunderCvHonor, type FunderCvItemKind, type FunderCvPerson, type FunderCvProfileInput, type FunderCvSelection, type JsonResume, type JsonResumeAward, type JsonResumeBasics, type JsonResumeCertificate, type JsonResumeEducation, type JsonResumeLanguage, type JsonResumeLocation, type JsonResumeMeta, type JsonResumeOptions, type JsonResumeProfileInput, type JsonResumeProfileLink, type JsonResumeProject, type JsonResumePublication, type JsonResumeSkill, type JsonResumeVolunteer, type JsonResumeWork, type NihEducation, type NihFunderCv, type Sanitizer, defaultFunderCvSelection, findUnknownFunderCvItems, funderCvItemId, funderCvSelectionSchema, listFunderCvCandidates, profileToFunderCv, profileToJsonResume, toResumeDate };
516
+ export { EXPORT_CONTACT_LIMITS, type ErcContribution, type ErcEducation, type ErcFunderCv, type ErcFunderCvPerson, type ErcRecognition, type ExportContact, type ExportContactDraft, FUNDER_CV_FORMATS, FUNDER_CV_GROUPS, FUNDER_CV_ITEM_KINDS, type FunderCvAppointment, type FunderCvCandidate, type FunderCvCitation, type FunderCvDocument, type FunderCvFormat, type FunderCvGroupName, type FunderCvGroupSpec, type FunderCvHonor, type FunderCvItemKind, type FunderCvOptions, type FunderCvPerson, type FunderCvProfileInput, type FunderCvSelection, type JsonResume, type JsonResumeAward, type JsonResumeBasics, type JsonResumeCertificate, type JsonResumeEducation, type JsonResumeLanguage, type JsonResumeLocation, type JsonResumeMeta, type JsonResumeOptions, type JsonResumeProfileInput, type JsonResumeProfileLink, type JsonResumeProject, type JsonResumePublication, type JsonResumeSkill, type JsonResumeVolunteer, type JsonResumeWork, type NihEducation, type NihFunderCv, type Sanitizer, defaultFunderCvSelection, exportContactSchema, findUnknownFunderCvItems, funderCvItemId, funderCvSelectionSchema, listFunderCvCandidates, looksLikeEmail, normalizeExportContact, pickExportContact, profileToFunderCv, profileToJsonResume, toResumeDate };
@@ -1,4 +1,11 @@
1
1
  // src/resume/index.ts
2
+ import {
3
+ EXPORT_CONTACT_LIMITS,
4
+ exportContactSchema,
5
+ looksLikeEmail,
6
+ normalizeExportContact,
7
+ pickExportContact
8
+ } from "./export-contact.js";
2
9
  import { profileToJsonResume, toResumeDate } from "./json-resume.js";
3
10
  import {
4
11
  FUNDER_CV_FORMATS,
@@ -12,14 +19,19 @@ import {
12
19
  profileToFunderCv
13
20
  } from "./funder-cv.js";
14
21
  export {
22
+ EXPORT_CONTACT_LIMITS,
15
23
  FUNDER_CV_FORMATS,
16
24
  FUNDER_CV_GROUPS,
17
25
  FUNDER_CV_ITEM_KINDS,
18
26
  defaultFunderCvSelection,
27
+ exportContactSchema,
19
28
  findUnknownFunderCvItems,
20
29
  funderCvItemId,
21
30
  funderCvSelectionSchema,
22
31
  listFunderCvCandidates,
32
+ looksLikeEmail,
33
+ normalizeExportContact,
34
+ pickExportContact,
23
35
  profileToFunderCv,
24
36
  profileToJsonResume,
25
37
  toResumeDate
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/resume/index.ts"],"sourcesContent":["export { profileToJsonResume, toResumeDate } from './json-resume.js';\nexport type {\n JsonResume,\n JsonResumeAward,\n JsonResumeBasics,\n JsonResumeCertificate,\n JsonResumeEducation,\n JsonResumeLanguage,\n JsonResumeLocation,\n JsonResumeMeta,\n JsonResumeOptions,\n JsonResumeProfileInput,\n JsonResumeProfileLink,\n JsonResumeProject,\n JsonResumePublication,\n JsonResumeSkill,\n JsonResumeVolunteer,\n JsonResumeWork,\n Sanitizer,\n} from './json-resume.js';\nexport {\n FUNDER_CV_FORMATS,\n FUNDER_CV_GROUPS,\n FUNDER_CV_ITEM_KINDS,\n defaultFunderCvSelection,\n findUnknownFunderCvItems,\n funderCvItemId,\n funderCvSelectionSchema,\n listFunderCvCandidates,\n profileToFunderCv,\n} from './funder-cv.js';\nexport type {\n ErcContribution,\n ErcEducation,\n ErcFunderCv,\n ErcRecognition,\n FunderCvAppointment,\n FunderCvCandidate,\n FunderCvCitation,\n FunderCvDocument,\n FunderCvFormat,\n FunderCvGroupName,\n FunderCvGroupSpec,\n FunderCvHonor,\n FunderCvItemKind,\n FunderCvPerson,\n FunderCvProfileInput,\n FunderCvSelection,\n NihEducation,\n NihFunderCv,\n} from './funder-cv.js';\n"],"mappings":";AAAA,SAAS,qBAAqB,oBAAoB;AAoBlD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;","names":[]}
1
+ {"version":3,"sources":["../../src/resume/index.ts"],"sourcesContent":["export {\n EXPORT_CONTACT_LIMITS,\n exportContactSchema,\n looksLikeEmail,\n normalizeExportContact,\n pickExportContact,\n} from './export-contact.js';\nexport type { ExportContact, ExportContactDraft } from './export-contact.js';\nexport { profileToJsonResume, toResumeDate } from './json-resume.js';\nexport type {\n JsonResume,\n JsonResumeAward,\n JsonResumeBasics,\n JsonResumeCertificate,\n JsonResumeEducation,\n JsonResumeLanguage,\n JsonResumeLocation,\n JsonResumeMeta,\n JsonResumeOptions,\n JsonResumeProfileInput,\n JsonResumeProfileLink,\n JsonResumeProject,\n JsonResumePublication,\n JsonResumeSkill,\n JsonResumeVolunteer,\n JsonResumeWork,\n Sanitizer,\n} from './json-resume.js';\nexport {\n FUNDER_CV_FORMATS,\n FUNDER_CV_GROUPS,\n FUNDER_CV_ITEM_KINDS,\n defaultFunderCvSelection,\n findUnknownFunderCvItems,\n funderCvItemId,\n funderCvSelectionSchema,\n listFunderCvCandidates,\n profileToFunderCv,\n} from './funder-cv.js';\nexport type {\n ErcContribution,\n ErcEducation,\n ErcFunderCv,\n ErcFunderCvPerson,\n ErcRecognition,\n FunderCvAppointment,\n FunderCvCandidate,\n FunderCvCitation,\n FunderCvDocument,\n FunderCvFormat,\n FunderCvGroupName,\n FunderCvGroupSpec,\n FunderCvHonor,\n FunderCvItemKind,\n FunderCvOptions,\n FunderCvPerson,\n FunderCvProfileInput,\n FunderCvSelection,\n NihEducation,\n NihFunderCv,\n} from './funder-cv.js';\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,qBAAqB,oBAAoB;AAoBlD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;","names":[]}
@@ -2,6 +2,7 @@
2
2
  import { formatStructuredName } from "../format/pds-utils.js";
3
3
  import { filterHidden } from "../profile/section-model.js";
4
4
  import { normaliseBaseUrl, profileUrl } from "../jsonld/url.js";
5
+ import { normalizeExportContact } from "./export-contact.js";
5
6
  var SCHEMA_VERSION = "v1.0.0";
6
7
  var RESUME_DATE_RE = /^(\d{4})(-\d{2})?(-\d{2})?/;
7
8
  function toResumeDate(value) {
@@ -21,6 +22,9 @@ var identity = (input) => input;
21
22
  function compact(value) {
22
23
  return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
23
24
  }
25
+ function withAddress(location, address) {
26
+ return address ? { address, ...location } : location;
27
+ }
24
28
  function section(key, items) {
25
29
  return items.length > 0 ? { [key]: items } : void 0;
26
30
  }
@@ -45,6 +49,7 @@ function profileToJsonResume(profile, options = {}) {
45
49
  const s = options.sanitize ?? identity;
46
50
  const baseUrl = normaliseBaseUrl(options.baseUrl);
47
51
  const canonical = options.canonicalUrl ?? profileUrl(baseUrl, profile.handle);
52
+ const contact = normalizeExportContact(options.contact ?? {});
48
53
  const name = formatStructuredName(profile.givenName, profile.familyName) ?? text(profile.displayName, s) ?? profile.handle;
49
54
  const profiles = filterHidden(profile.externalAccounts).map(
50
55
  (account) => compact({
@@ -57,9 +62,14 @@ function profileToJsonResume(profile, options = {}) {
57
62
  name,
58
63
  label: text(profile.headline, s),
59
64
  image: text(profile.avatar, s),
65
+ email: text(contact.email, s),
66
+ phone: text(contact.phone, s),
67
+ driversLicense: text(contact.driversLicense, s),
68
+ rightToWork: text(contact.rightToWork, s),
69
+ nationality: text(contact.nationality, s),
60
70
  url: text(profile.website, s) ?? canonical,
61
71
  summary: text(profile.about, s),
62
- location: mapLocation(profile.location, s),
72
+ location: withAddress(mapLocation(profile.location, s), text(contact.address, s)),
63
73
  ...section("profiles", profiles)
64
74
  });
65
75
  const work = filterHidden(profile.positions).map(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/resume/json-resume.ts"],"sourcesContent":["/**\n * JSON Resume v1.0.0 emitter for a Sifa profile.\n *\n * https://jsonresume.org/schema/\n *\n * One pure function, no fetching, no DOM, no framework -- the same shape as the\n * JSON-LD emitter next door, and for the same reason: sifa.id, page.sifa.id and\n * any future consumer should emit one document from one implementation.\n *\n * Why this format earns its place: it is the lingua franca of CV tooling, so\n * emitting it hands a user every JSON Resume theme, and the Typst CV templates\n * on Typst Universe, without Sifa writing a renderer for any of them.\n *\n * Fields Sifa holds that JSON Resume has no slot for -- endorsements,\n * confirmations, involvement, investments, presentations, courses -- are\n * dropped rather than smuggled into a nearby field. The loss is documented for\n * the user at the point of download.\n */\n\nimport { formatStructuredName } from '../format/pds-utils.js';\nimport { filterHidden } from '../profile/section-model.js';\nimport type {\n ExternalAccount,\n LanguageProficiency,\n LocationValue,\n ProfileCertification,\n ProfileEducation,\n ProfileHonor,\n ProfileLanguage,\n ProfilePosition,\n ProfileProject,\n ProfilePublication,\n ProfileSkill,\n ProfileVolunteering,\n} from '../types/index.js';\nimport { normaliseBaseUrl, profileUrl } from '../jsonld/url.js';\n\nexport type Sanitizer = (input: string) => string;\n\nexport interface JsonResumeOptions {\n /** Canonical origin for the profile URL. Defaults to `https://sifa.id`. */\n readonly baseUrl?: string;\n /** Applied to every user-authored string. Defaults to identity. */\n readonly sanitize?: Sanitizer;\n /**\n * Absolute URL of the profile this resume describes. Defaults to\n * `${baseUrl}/p/${handle}`. A personal site served from a custom domain is\n * not at that path.\n */\n readonly canonicalUrl?: string;\n}\n\n/**\n * The subset of `Profile` this emitter reads. Deliberately structural rather\n * than the full `Profile`, so a caller holding a partially hydrated profile --\n * or a test -- does not have to invent forty unrelated fields.\n */\nexport interface JsonResumeProfileInput {\n readonly handle: string;\n readonly displayName?: string;\n readonly givenName?: string;\n readonly familyName?: string;\n readonly avatar?: string;\n readonly headline?: string;\n readonly about?: string;\n readonly website?: string;\n readonly location?: LocationValue | null;\n readonly positions?: ProfilePosition[];\n readonly education?: ProfileEducation[];\n readonly skills?: ProfileSkill[];\n readonly certifications?: ProfileCertification[];\n readonly projects?: ProfileProject[];\n readonly publications?: ProfilePublication[];\n readonly volunteering?: ProfileVolunteering[];\n readonly honors?: ProfileHonor[];\n readonly languages?: ProfileLanguage[];\n readonly externalAccounts?: ExternalAccount[];\n}\n\nexport interface JsonResumeLocation {\n readonly city?: string;\n readonly region?: string;\n readonly countryCode?: string;\n readonly postalCode?: string;\n}\n\nexport interface JsonResumeProfileLink {\n readonly network: string;\n readonly username?: string;\n readonly url?: string;\n}\n\nexport interface JsonResumeBasics {\n readonly name: string;\n readonly label?: string;\n readonly image?: string;\n readonly url?: string;\n readonly summary?: string;\n readonly location?: JsonResumeLocation;\n readonly profiles?: JsonResumeProfileLink[];\n}\n\nexport interface JsonResumeWork {\n readonly name?: string;\n readonly position: string;\n readonly summary?: string;\n readonly startDate?: string;\n readonly endDate?: string;\n}\n\nexport interface JsonResumeVolunteer {\n /**\n * Omitted when the record carries no organization name at all. Emitting an\n * empty string would render as a blank organization line in a CV template.\n */\n readonly organization?: string;\n readonly position?: string;\n readonly summary?: string;\n readonly startDate?: string;\n readonly endDate?: string;\n}\n\nexport interface JsonResumeEducation {\n /** Omitted when the record carries no institution name. See volunteer. */\n readonly institution?: string;\n readonly area?: string;\n readonly studyType?: string;\n readonly startDate?: string;\n readonly endDate?: string;\n}\n\nexport interface JsonResumeAward {\n readonly title: string;\n readonly date?: string;\n readonly awarder?: string;\n readonly summary?: string;\n}\n\nexport interface JsonResumeCertificate {\n readonly name: string;\n readonly date?: string;\n readonly issuer?: string;\n readonly url?: string;\n}\n\nexport interface JsonResumePublication {\n readonly name: string;\n readonly publisher?: string;\n readonly releaseDate?: string;\n readonly url?: string;\n readonly summary?: string;\n}\n\nexport interface JsonResumeSkill {\n readonly name: string;\n}\n\nexport interface JsonResumeLanguage {\n readonly language: string;\n readonly fluency?: string;\n}\n\nexport interface JsonResumeProject {\n readonly name: string;\n readonly description?: string;\n readonly url?: string;\n readonly startDate?: string;\n readonly endDate?: string;\n}\n\nexport interface JsonResumeMeta {\n readonly canonical: string;\n readonly version: string;\n readonly lastModified?: string;\n}\n\nexport interface JsonResume {\n readonly basics?: JsonResumeBasics;\n readonly work?: JsonResumeWork[];\n readonly volunteer?: JsonResumeVolunteer[];\n readonly education?: JsonResumeEducation[];\n readonly awards?: JsonResumeAward[];\n readonly certificates?: JsonResumeCertificate[];\n readonly publications?: JsonResumePublication[];\n readonly skills?: JsonResumeSkill[];\n readonly languages?: JsonResumeLanguage[];\n readonly projects?: JsonResumeProject[];\n readonly meta?: JsonResumeMeta;\n}\n\n/** Schema version this emitter targets, as JSON Resume writes it in `meta`. */\nconst SCHEMA_VERSION = 'v1.0.0';\n\n/**\n * A date JSON Resume will accept: `YYYY`, `YYYY-MM` or `YYYY-MM-DD`. Sifa\n * stores RFC 3339 for some fields, and a full timestamp pasted into a CV\n * template renders as `2020-03-04T00:00:00.000Z` on the page.\n */\nconst RESUME_DATE_RE = /^(\\d{4})(-\\d{2})?(-\\d{2})?/;\n\nexport function toResumeDate(value: string | undefined): string | undefined {\n const trimmed = value?.trim();\n if (!trimmed) return undefined;\n const match = RESUME_DATE_RE.exec(trimmed);\n return match ? match[0] : undefined;\n}\n\n/**\n * JSON Resume's `fluency` is free text, so these are display labels rather\n * than a mapped vocabulary. Kept in one place so export and a future import\n * agree on the spelling.\n */\nconst FLUENCY_LABELS: Record<LanguageProficiency, string> = {\n elementary: 'Elementary',\n limited_working: 'Limited working',\n professional_working: 'Professional working',\n full_professional: 'Full professional',\n native: 'Native or bilingual',\n};\n\nconst identity: Sanitizer = (input) => input;\n\n/** Drops keys whose value is undefined, so the document carries no empty slots. */\nfunction compact<T extends object>(value: T): T {\n return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as T;\n}\n\n/** Omits the key entirely when the mapped list came out empty. */\nfunction section<K extends string, T>(key: K, items: T[]): Record<K, T[]> | undefined {\n return items.length > 0 ? ({ [key]: items } as Record<K, T[]>) : undefined;\n}\n\nfunction mapLocation(\n location: LocationValue | null | undefined,\n s: Sanitizer,\n): JsonResumeLocation | undefined {\n if (!location) return undefined;\n const mapped = compact({\n city: text(location.locality ?? location.city, s),\n region: text(location.region, s),\n countryCode: text(location.countryCode, s),\n postalCode: text(location.postalCode, s),\n });\n return Object.keys(mapped).length > 0 ? mapped : undefined;\n}\n\n/** Trimmed, sanitised, and undefined when there is nothing left. */\nfunction text(value: string | undefined, s: Sanitizer): string | undefined {\n const trimmed = value?.trim();\n return trimmed ? s(trimmed) : undefined;\n}\n\n/**\n * The name to print for a linked organization. `entityName` is the AppView's\n * resolved current name and wins when present, matching how the profile itself\n * renders these.\n */\nfunction orgName(\n entityName: string | undefined,\n stored: string | undefined,\n s: Sanitizer,\n): string | undefined {\n return text(entityName, s) ?? text(stored, s);\n}\n\n/**\n * Build a JSON Resume v1.0.0 document from a Sifa profile.\n *\n * Hidden records are dropped. Keys are omitted rather than emitted empty, so a\n * profile with no publications produces a document with no `publications` key\n * instead of one a reader has to interpret.\n */\nexport function profileToJsonResume(\n profile: JsonResumeProfileInput,\n options: JsonResumeOptions = {},\n): JsonResume {\n const s = options.sanitize ?? identity;\n const baseUrl = normaliseBaseUrl(options.baseUrl);\n const canonical = options.canonicalUrl ?? profileUrl(baseUrl, profile.handle);\n\n const name =\n formatStructuredName(profile.givenName, profile.familyName) ??\n text(profile.displayName, s) ??\n profile.handle;\n\n const profiles = filterHidden(profile.externalAccounts).map((account) =>\n compact<JsonResumeProfileLink>({\n network: s(account.platform),\n username: text(account.label, s),\n url: text(account.url, s),\n }),\n );\n\n const basics = compact<JsonResumeBasics>({\n name,\n label: text(profile.headline, s),\n image: text(profile.avatar, s),\n url: text(profile.website, s) ?? canonical,\n summary: text(profile.about, s),\n location: mapLocation(profile.location, s),\n ...section('profiles', profiles),\n });\n\n const work = filterHidden(profile.positions).map((position) =>\n compact<JsonResumeWork>({\n name: orgName(position.entityName, position.company, s),\n position: s(position.title),\n summary: text(position.description, s),\n startDate: toResumeDate(position.startedAt),\n endDate: toResumeDate(position.endedAt),\n }),\n );\n\n const volunteer = filterHidden(profile.volunteering).map((entry) =>\n compact<JsonResumeVolunteer>({\n organization: orgName(entry.entityName, entry.organization, s),\n position: text(entry.role, s),\n summary: text(entry.description, s),\n startDate: toResumeDate(entry.startDate),\n endDate: toResumeDate(entry.endDate),\n }),\n );\n\n const education = filterHidden(profile.education).map((entry) =>\n compact<JsonResumeEducation>({\n institution: orgName(entry.entityName, entry.institution, s),\n area: text(entry.fieldOfStudy, s),\n studyType: text(entry.degree, s),\n startDate: toResumeDate(entry.startedAt),\n endDate: toResumeDate(entry.endedAt),\n }),\n );\n\n const awards = filterHidden(profile.honors).map((honor) =>\n compact<JsonResumeAward>({\n title: s(honor.title),\n date: toResumeDate(honor.date),\n awarder: orgName(honor.entityName, honor.issuer, s),\n summary: text(honor.description, s),\n }),\n );\n\n const certificates = filterHidden(profile.certifications).map((cert) =>\n compact<JsonResumeCertificate>({\n name: s(cert.name),\n date: toResumeDate(cert.issueDate),\n // `issuingOrg` is the deprecated read-view alias for `authority`; records\n // indexed before the rename carry only the alias.\n issuer: orgName(cert.entityName, cert.authority ?? cert.issuingOrg, s),\n url: text(cert.credentialUrl, s),\n }),\n );\n\n const publications = filterHidden(profile.publications).map((publication) =>\n compact<JsonResumePublication>({\n name: s(publication.title),\n publisher: text(publication.publisher, s),\n releaseDate: toResumeDate(publication.date),\n url: text(publication.url, s),\n summary: text(publication.description, s),\n }),\n );\n\n // One entry per skill rather than JSON Resume's grouped shape: Sifa stores one\n // record per skill, and keeping the two symmetric is what makes a round-trip\n // through a third-party editor safe. Category and endorsement counts have no\n // JSON Resume slot and are dropped. Skill records carry no `hidden` flag, so\n // there is nothing to filter here.\n const skills = (profile.skills ?? []).map((skill) => ({ name: s(skill.name) }));\n\n const languages = filterHidden(profile.languages).map((language) =>\n compact<JsonResumeLanguage>({\n language: s(language.language),\n fluency: language.proficiency ? FLUENCY_LABELS[language.proficiency] : undefined,\n }),\n );\n\n const projects = filterHidden(profile.projects).map((project) =>\n compact<JsonResumeProject>({\n name: s(project.name),\n description: text(project.description, s),\n url: text(project.url, s),\n startDate: toResumeDate(project.startDate),\n endDate: toResumeDate(project.endDate),\n }),\n );\n\n return {\n basics,\n ...section('work', work),\n ...section('volunteer', volunteer),\n ...section('education', education),\n ...section('awards', awards),\n ...section('certificates', certificates),\n ...section('publications', publications),\n ...section('skills', skills),\n ...section('languages', languages),\n ...section('projects', projects),\n meta: { canonical, version: SCHEMA_VERSION },\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/resume/json-resume.ts"],"sourcesContent":["/**\n * JSON Resume v1.0.0 emitter for a Sifa profile.\n *\n * https://jsonresume.org/schema/\n *\n * One pure function, no fetching, no DOM, no framework -- the same shape as the\n * JSON-LD emitter next door, and for the same reason: sifa.id, page.sifa.id and\n * any future consumer should emit one document from one implementation.\n *\n * Why this format earns its place: it is the lingua franca of CV tooling, so\n * emitting it hands a user every JSON Resume theme, and the Typst CV templates\n * on Typst Universe, without Sifa writing a renderer for any of them.\n *\n * Fields Sifa holds that JSON Resume has no slot for -- endorsements,\n * confirmations, involvement, investments, presentations, courses -- are\n * dropped rather than smuggled into a nearby field. The loss is documented for\n * the user at the point of download.\n */\n\nimport { formatStructuredName } from '../format/pds-utils.js';\nimport { filterHidden } from '../profile/section-model.js';\nimport type {\n ExternalAccount,\n LanguageProficiency,\n LocationValue,\n ProfileCertification,\n ProfileEducation,\n ProfileHonor,\n ProfileLanguage,\n ProfilePosition,\n ProfileProject,\n ProfilePublication,\n ProfileSkill,\n ProfileVolunteering,\n} from '../types/index.js';\nimport { normaliseBaseUrl, profileUrl } from '../jsonld/url.js';\nimport { normalizeExportContact, type ExportContact } from './export-contact.js';\n\nexport type Sanitizer = (input: string) => string;\n\nexport interface JsonResumeOptions {\n /** Canonical origin for the profile URL. Defaults to `https://sifa.id`. */\n readonly baseUrl?: string;\n /** Applied to every user-authored string. Defaults to identity. */\n readonly sanitize?: Sanitizer;\n /**\n * Absolute URL of the profile this resume describes. Defaults to\n * `${baseUrl}/p/${handle}`. A personal site served from a custom domain is\n * not at that path.\n */\n readonly canonicalUrl?: string;\n /**\n * Email, phone and postal address to print. Sifa holds none of these; the\n * owner types them on the export page. Without it the resume has no\n * `email`, `phone` or `location.address`.\n */\n readonly contact?: ExportContact;\n}\n\n/**\n * The subset of `Profile` this emitter reads. Deliberately structural rather\n * than the full `Profile`, so a caller holding a partially hydrated profile --\n * or a test -- does not have to invent forty unrelated fields.\n */\nexport interface JsonResumeProfileInput {\n readonly handle: string;\n readonly displayName?: string;\n readonly givenName?: string;\n readonly familyName?: string;\n readonly avatar?: string;\n readonly headline?: string;\n readonly about?: string;\n readonly website?: string;\n readonly location?: LocationValue | null;\n readonly positions?: ProfilePosition[];\n readonly education?: ProfileEducation[];\n readonly skills?: ProfileSkill[];\n readonly certifications?: ProfileCertification[];\n readonly projects?: ProfileProject[];\n readonly publications?: ProfilePublication[];\n readonly volunteering?: ProfileVolunteering[];\n readonly honors?: ProfileHonor[];\n readonly languages?: ProfileLanguage[];\n readonly externalAccounts?: ExternalAccount[];\n}\n\nexport interface JsonResumeLocation {\n /** Street address; lines are separated by `\\n`, as the schema asks. */\n readonly address?: string;\n readonly city?: string;\n readonly region?: string;\n readonly countryCode?: string;\n readonly postalCode?: string;\n}\n\nexport interface JsonResumeProfileLink {\n readonly network: string;\n readonly username?: string;\n readonly url?: string;\n}\n\nexport interface JsonResumeBasics {\n readonly name: string;\n readonly label?: string;\n readonly image?: string;\n readonly url?: string;\n readonly email?: string;\n readonly phone?: string;\n /**\n * Not in the JSON Resume schema. Templates that do not know these keys\n * ignore them; Sifa's own Typst CV prints them as personal details.\n */\n readonly driversLicense?: string;\n readonly rightToWork?: string;\n readonly nationality?: string;\n readonly summary?: string;\n readonly location?: JsonResumeLocation;\n readonly profiles?: JsonResumeProfileLink[];\n}\n\nexport interface JsonResumeWork {\n readonly name?: string;\n readonly position: string;\n readonly summary?: string;\n readonly startDate?: string;\n readonly endDate?: string;\n}\n\nexport interface JsonResumeVolunteer {\n /**\n * Omitted when the record carries no organization name at all. Emitting an\n * empty string would render as a blank organization line in a CV template.\n */\n readonly organization?: string;\n readonly position?: string;\n readonly summary?: string;\n readonly startDate?: string;\n readonly endDate?: string;\n}\n\nexport interface JsonResumeEducation {\n /** Omitted when the record carries no institution name. See volunteer. */\n readonly institution?: string;\n readonly area?: string;\n readonly studyType?: string;\n readonly startDate?: string;\n readonly endDate?: string;\n}\n\nexport interface JsonResumeAward {\n readonly title: string;\n readonly date?: string;\n readonly awarder?: string;\n readonly summary?: string;\n}\n\nexport interface JsonResumeCertificate {\n readonly name: string;\n readonly date?: string;\n readonly issuer?: string;\n readonly url?: string;\n}\n\nexport interface JsonResumePublication {\n readonly name: string;\n readonly publisher?: string;\n readonly releaseDate?: string;\n readonly url?: string;\n readonly summary?: string;\n}\n\nexport interface JsonResumeSkill {\n readonly name: string;\n}\n\nexport interface JsonResumeLanguage {\n readonly language: string;\n readonly fluency?: string;\n}\n\nexport interface JsonResumeProject {\n readonly name: string;\n readonly description?: string;\n readonly url?: string;\n readonly startDate?: string;\n readonly endDate?: string;\n}\n\nexport interface JsonResumeMeta {\n readonly canonical: string;\n readonly version: string;\n readonly lastModified?: string;\n}\n\nexport interface JsonResume {\n readonly basics?: JsonResumeBasics;\n readonly work?: JsonResumeWork[];\n readonly volunteer?: JsonResumeVolunteer[];\n readonly education?: JsonResumeEducation[];\n readonly awards?: JsonResumeAward[];\n readonly certificates?: JsonResumeCertificate[];\n readonly publications?: JsonResumePublication[];\n readonly skills?: JsonResumeSkill[];\n readonly languages?: JsonResumeLanguage[];\n readonly projects?: JsonResumeProject[];\n readonly meta?: JsonResumeMeta;\n}\n\n/** Schema version this emitter targets, as JSON Resume writes it in `meta`. */\nconst SCHEMA_VERSION = 'v1.0.0';\n\n/**\n * A date JSON Resume will accept: `YYYY`, `YYYY-MM` or `YYYY-MM-DD`. Sifa\n * stores RFC 3339 for some fields, and a full timestamp pasted into a CV\n * template renders as `2020-03-04T00:00:00.000Z` on the page.\n */\nconst RESUME_DATE_RE = /^(\\d{4})(-\\d{2})?(-\\d{2})?/;\n\nexport function toResumeDate(value: string | undefined): string | undefined {\n const trimmed = value?.trim();\n if (!trimmed) return undefined;\n const match = RESUME_DATE_RE.exec(trimmed);\n return match ? match[0] : undefined;\n}\n\n/**\n * JSON Resume's `fluency` is free text, so these are display labels rather\n * than a mapped vocabulary. Kept in one place so export and a future import\n * agree on the spelling.\n */\nconst FLUENCY_LABELS: Record<LanguageProficiency, string> = {\n elementary: 'Elementary',\n limited_working: 'Limited working',\n professional_working: 'Professional working',\n full_professional: 'Full professional',\n native: 'Native or bilingual',\n};\n\nconst identity: Sanitizer = (input) => input;\n\n/** Drops keys whose value is undefined, so the document carries no empty slots. */\nfunction compact<T extends object>(value: T): T {\n return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as T;\n}\n\n/** Adds the owner's street address to the profile's location. */\nfunction withAddress(\n location: JsonResumeLocation | undefined,\n address: string | undefined,\n): JsonResumeLocation | undefined {\n return address ? { address, ...location } : location;\n}\n\n/** Omits the key entirely when the mapped list came out empty. */\nfunction section<K extends string, T>(key: K, items: T[]): Record<K, T[]> | undefined {\n return items.length > 0 ? ({ [key]: items } as Record<K, T[]>) : undefined;\n}\n\nfunction mapLocation(\n location: LocationValue | null | undefined,\n s: Sanitizer,\n): JsonResumeLocation | undefined {\n if (!location) return undefined;\n const mapped = compact({\n city: text(location.locality ?? location.city, s),\n region: text(location.region, s),\n countryCode: text(location.countryCode, s),\n postalCode: text(location.postalCode, s),\n });\n return Object.keys(mapped).length > 0 ? mapped : undefined;\n}\n\n/** Trimmed, sanitised, and undefined when there is nothing left. */\nfunction text(value: string | undefined, s: Sanitizer): string | undefined {\n const trimmed = value?.trim();\n return trimmed ? s(trimmed) : undefined;\n}\n\n/**\n * The name to print for a linked organization. `entityName` is the AppView's\n * resolved current name and wins when present, matching how the profile itself\n * renders these.\n */\nfunction orgName(\n entityName: string | undefined,\n stored: string | undefined,\n s: Sanitizer,\n): string | undefined {\n return text(entityName, s) ?? text(stored, s);\n}\n\n/**\n * Build a JSON Resume v1.0.0 document from a Sifa profile.\n *\n * Hidden records are dropped. Keys are omitted rather than emitted empty, so a\n * profile with no publications produces a document with no `publications` key\n * instead of one a reader has to interpret.\n */\nexport function profileToJsonResume(\n profile: JsonResumeProfileInput,\n options: JsonResumeOptions = {},\n): JsonResume {\n const s = options.sanitize ?? identity;\n const baseUrl = normaliseBaseUrl(options.baseUrl);\n const canonical = options.canonicalUrl ?? profileUrl(baseUrl, profile.handle);\n const contact = normalizeExportContact(options.contact ?? {});\n\n const name =\n formatStructuredName(profile.givenName, profile.familyName) ??\n text(profile.displayName, s) ??\n profile.handle;\n\n const profiles = filterHidden(profile.externalAccounts).map((account) =>\n compact<JsonResumeProfileLink>({\n network: s(account.platform),\n username: text(account.label, s),\n url: text(account.url, s),\n }),\n );\n\n const basics = compact<JsonResumeBasics>({\n name,\n label: text(profile.headline, s),\n image: text(profile.avatar, s),\n email: text(contact.email, s),\n phone: text(contact.phone, s),\n driversLicense: text(contact.driversLicense, s),\n rightToWork: text(contact.rightToWork, s),\n nationality: text(contact.nationality, s),\n url: text(profile.website, s) ?? canonical,\n summary: text(profile.about, s),\n location: withAddress(mapLocation(profile.location, s), text(contact.address, s)),\n ...section('profiles', profiles),\n });\n\n const work = filterHidden(profile.positions).map((position) =>\n compact<JsonResumeWork>({\n name: orgName(position.entityName, position.company, s),\n position: s(position.title),\n summary: text(position.description, s),\n startDate: toResumeDate(position.startedAt),\n endDate: toResumeDate(position.endedAt),\n }),\n );\n\n const volunteer = filterHidden(profile.volunteering).map((entry) =>\n compact<JsonResumeVolunteer>({\n organization: orgName(entry.entityName, entry.organization, s),\n position: text(entry.role, s),\n summary: text(entry.description, s),\n startDate: toResumeDate(entry.startDate),\n endDate: toResumeDate(entry.endDate),\n }),\n );\n\n const education = filterHidden(profile.education).map((entry) =>\n compact<JsonResumeEducation>({\n institution: orgName(entry.entityName, entry.institution, s),\n area: text(entry.fieldOfStudy, s),\n studyType: text(entry.degree, s),\n startDate: toResumeDate(entry.startedAt),\n endDate: toResumeDate(entry.endedAt),\n }),\n );\n\n const awards = filterHidden(profile.honors).map((honor) =>\n compact<JsonResumeAward>({\n title: s(honor.title),\n date: toResumeDate(honor.date),\n awarder: orgName(honor.entityName, honor.issuer, s),\n summary: text(honor.description, s),\n }),\n );\n\n const certificates = filterHidden(profile.certifications).map((cert) =>\n compact<JsonResumeCertificate>({\n name: s(cert.name),\n date: toResumeDate(cert.issueDate),\n // `issuingOrg` is the deprecated read-view alias for `authority`; records\n // indexed before the rename carry only the alias.\n issuer: orgName(cert.entityName, cert.authority ?? cert.issuingOrg, s),\n url: text(cert.credentialUrl, s),\n }),\n );\n\n const publications = filterHidden(profile.publications).map((publication) =>\n compact<JsonResumePublication>({\n name: s(publication.title),\n publisher: text(publication.publisher, s),\n releaseDate: toResumeDate(publication.date),\n url: text(publication.url, s),\n summary: text(publication.description, s),\n }),\n );\n\n // One entry per skill rather than JSON Resume's grouped shape: Sifa stores one\n // record per skill, and keeping the two symmetric is what makes a round-trip\n // through a third-party editor safe. Category and endorsement counts have no\n // JSON Resume slot and are dropped. Skill records carry no `hidden` flag, so\n // there is nothing to filter here.\n const skills = (profile.skills ?? []).map((skill) => ({ name: s(skill.name) }));\n\n const languages = filterHidden(profile.languages).map((language) =>\n compact<JsonResumeLanguage>({\n language: s(language.language),\n fluency: language.proficiency ? FLUENCY_LABELS[language.proficiency] : undefined,\n }),\n );\n\n const projects = filterHidden(profile.projects).map((project) =>\n compact<JsonResumeProject>({\n name: s(project.name),\n description: text(project.description, s),\n url: text(project.url, s),\n startDate: toResumeDate(project.startDate),\n endDate: toResumeDate(project.endDate),\n }),\n );\n\n return {\n basics,\n ...section('work', work),\n ...section('volunteer', volunteer),\n ...section('education', education),\n ...section('awards', awards),\n ...section('certificates', certificates),\n ...section('publications', publications),\n ...section('skills', skills),\n ...section('languages', languages),\n ...section('projects', projects),\n meta: { canonical, version: SCHEMA_VERSION },\n };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@singi-labs/sifa-sdk",
3
- "version": "0.19.89",
3
+ "version": "0.19.91",
4
4
  "description": "Sifa SDK — public client library for the Sifa AppView on AT Protocol. Shared by sifa-web and sifa-app.",
5
5
  "license": "MIT",
6
6
  "type": "module",