@singi-labs/sifa-sdk 0.19.93 → 0.19.95

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/README.md CHANGED
@@ -27,16 +27,17 @@ Published publicly so third-party developers can build their own AT Protocol cli
27
27
 
28
28
  ## What this package contains
29
29
 
30
- | Area | What it provides |
31
- | ----------- | ------------------------------------------------------------------------------------- |
32
- | Types | TypeScript types for Sifa data structures, generated from `sifa-lexicons` |
33
- | Schemas | Zod schemas mirroring lexicon constraints for runtime validation |
34
- | Query layer | TanStack Query keys, fetchers, and React hooks for calling `sifa-api` |
35
- | ATproto | Wrapper around `@atproto/api` that accepts an authenticated agent |
36
- | Format | Pure formatters for dates, locations, profile completeness |
37
- | Logic | Business-logic predicates (display-only; server-side trust logic lives in `sifa-api`) |
38
- | Taxonomy | Constants and enums (continents, industries, skill categories, activity tiers) |
39
- | Tokens | Sifa-brand design tokens, exported via `@singi-labs/sifa-sdk/tokens` (optional) |
30
+ | Area | What it provides |
31
+ | ----------- | ------------------------------------------------------------------------------------------------------- |
32
+ | Types | TypeScript types for Sifa data structures, generated from `sifa-lexicons` |
33
+ | Schemas | Zod schemas mirroring lexicon constraints for runtime validation |
34
+ | Query layer | TanStack Query keys, fetchers, and React hooks for calling `sifa-api` |
35
+ | ATproto | Wrapper around `@atproto/api` that accepts an authenticated agent |
36
+ | Format | Pure formatters for dates, locations, profile completeness |
37
+ | Logic | Business-logic predicates (display-only; server-side trust logic lives in `sifa-api`) |
38
+ | Taxonomy | Constants and enums (continents, industries, skill categories, activity tiers) |
39
+ | Tokens | Sifa-brand design tokens, exported via `@singi-labs/sifa-sdk/tokens` (optional) |
40
+ | Testing | Anonymized profile responses and sample export details, via `@singi-labs/sifa-sdk/testing` (tests only) |
40
41
 
41
42
  ---
42
43
 
@@ -0,0 +1,79 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Private contact and personal details for exports: email, phone, postal
5
+ * address, driver's license, right to work and nationality.
6
+ *
7
+ * Sifa never stores these and never writes them to the owner's PDS, where they
8
+ * would be public. The owner types them on the export page, the browser keeps
9
+ * them, and they go only into the files the owner downloads. This module holds
10
+ * the shape, the limits and the pick rule, so the export page and the server
11
+ * that builds a funder CV agree on them.
12
+ */
13
+
14
+ /** Longest value accepted per field. 254 is the longest valid email address. */
15
+ declare const EXPORT_CONTACT_LIMITS: {
16
+ readonly email: 254;
17
+ readonly phone: 40;
18
+ readonly address: 500;
19
+ readonly driversLicense: 60;
20
+ readonly rightToWork: 200;
21
+ readonly nationality: 100;
22
+ };
23
+ /** The contact details that go into an export. Absent keys are left out. */
24
+ interface ExportContact {
25
+ readonly email?: string;
26
+ readonly phone?: string;
27
+ /** Free text; lines are separated by `\n`. */
28
+ readonly address?: string;
29
+ /** License categories, e.g. "B, BE". */
30
+ readonly driversLicense?: string;
31
+ /** e.g. "EU citizen, no sponsorship needed". */
32
+ readonly rightToWork?: string;
33
+ readonly nationality?: string;
34
+ }
35
+ /** What the owner has typed, plus which fields they chose to include. */
36
+ interface ExportContactDraft {
37
+ readonly email?: string;
38
+ readonly phone?: string;
39
+ readonly address?: string;
40
+ readonly driversLicense?: string;
41
+ readonly rightToWork?: string;
42
+ readonly nationality?: string;
43
+ readonly includeEmail?: boolean;
44
+ readonly includePhone?: boolean;
45
+ readonly includeAddress?: boolean;
46
+ readonly includeDriversLicense?: boolean;
47
+ readonly includeRightToWork?: boolean;
48
+ readonly includeNationality?: boolean;
49
+ }
50
+ /**
51
+ * Validates a picked contact. Only lengths are checked: phone numbers and
52
+ * addresses vary too much between countries, and a wrong-looking email is a
53
+ * warning for the owner, not a reason to refuse the export.
54
+ */
55
+ declare const exportContactSchema: z.ZodObject<{
56
+ email: z.ZodOptional<z.ZodString>;
57
+ phone: z.ZodOptional<z.ZodString>;
58
+ address: z.ZodOptional<z.ZodString>;
59
+ driversLicense: z.ZodOptional<z.ZodString>;
60
+ rightToWork: z.ZodOptional<z.ZodString>;
61
+ nationality: z.ZodOptional<z.ZodString>;
62
+ }, z.core.$strict>;
63
+ /**
64
+ * The fields to put in an export: the included ones that are not blank, run
65
+ * through {@link normalizeExportContact}.
66
+ */
67
+ declare function pickExportContact(draft: ExportContactDraft): ExportContact;
68
+ /**
69
+ * Drops blank fields and tidies the rest. Every field but the address becomes
70
+ * one line with single spaces. An address keeps its line breaks but loses blank lines and the
71
+ * spaces around each line. Each field is cut at its {@link EXPORT_CONTACT_LIMITS}
72
+ * length. The emitters call this too, so a caller that skips
73
+ * {@link pickExportContact} and the schema gets the same bounded output.
74
+ */
75
+ declare function normalizeExportContact(contact: ExportContact): ExportContact;
76
+ /** A loose shape check for a non-blocking "check this address" hint. */
77
+ declare function looksLikeEmail(value: string): boolean;
78
+
79
+ export { type ExportContact as E, EXPORT_CONTACT_LIMITS as a, type ExportContactDraft as b, exportContactSchema as e, looksLikeEmail as l, normalizeExportContact as n, pickExportContact as p };
@@ -0,0 +1,79 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Private contact and personal details for exports: email, phone, postal
5
+ * address, driver's license, right to work and nationality.
6
+ *
7
+ * Sifa never stores these and never writes them to the owner's PDS, where they
8
+ * would be public. The owner types them on the export page, the browser keeps
9
+ * them, and they go only into the files the owner downloads. This module holds
10
+ * the shape, the limits and the pick rule, so the export page and the server
11
+ * that builds a funder CV agree on them.
12
+ */
13
+
14
+ /** Longest value accepted per field. 254 is the longest valid email address. */
15
+ declare const EXPORT_CONTACT_LIMITS: {
16
+ readonly email: 254;
17
+ readonly phone: 40;
18
+ readonly address: 500;
19
+ readonly driversLicense: 60;
20
+ readonly rightToWork: 200;
21
+ readonly nationality: 100;
22
+ };
23
+ /** The contact details that go into an export. Absent keys are left out. */
24
+ interface ExportContact {
25
+ readonly email?: string;
26
+ readonly phone?: string;
27
+ /** Free text; lines are separated by `\n`. */
28
+ readonly address?: string;
29
+ /** License categories, e.g. "B, BE". */
30
+ readonly driversLicense?: string;
31
+ /** e.g. "EU citizen, no sponsorship needed". */
32
+ readonly rightToWork?: string;
33
+ readonly nationality?: string;
34
+ }
35
+ /** What the owner has typed, plus which fields they chose to include. */
36
+ interface ExportContactDraft {
37
+ readonly email?: string;
38
+ readonly phone?: string;
39
+ readonly address?: string;
40
+ readonly driversLicense?: string;
41
+ readonly rightToWork?: string;
42
+ readonly nationality?: string;
43
+ readonly includeEmail?: boolean;
44
+ readonly includePhone?: boolean;
45
+ readonly includeAddress?: boolean;
46
+ readonly includeDriversLicense?: boolean;
47
+ readonly includeRightToWork?: boolean;
48
+ readonly includeNationality?: boolean;
49
+ }
50
+ /**
51
+ * Validates a picked contact. Only lengths are checked: phone numbers and
52
+ * addresses vary too much between countries, and a wrong-looking email is a
53
+ * warning for the owner, not a reason to refuse the export.
54
+ */
55
+ declare const exportContactSchema: z.ZodObject<{
56
+ email: z.ZodOptional<z.ZodString>;
57
+ phone: z.ZodOptional<z.ZodString>;
58
+ address: z.ZodOptional<z.ZodString>;
59
+ driversLicense: z.ZodOptional<z.ZodString>;
60
+ rightToWork: z.ZodOptional<z.ZodString>;
61
+ nationality: z.ZodOptional<z.ZodString>;
62
+ }, z.core.$strict>;
63
+ /**
64
+ * The fields to put in an export: the included ones that are not blank, run
65
+ * through {@link normalizeExportContact}.
66
+ */
67
+ declare function pickExportContact(draft: ExportContactDraft): ExportContact;
68
+ /**
69
+ * Drops blank fields and tidies the rest. Every field but the address becomes
70
+ * one line with single spaces. An address keeps its line breaks but loses blank lines and the
71
+ * spaces around each line. Each field is cut at its {@link EXPORT_CONTACT_LIMITS}
72
+ * length. The emitters call this too, so a caller that skips
73
+ * {@link pickExportContact} and the schema gets the same bounded output.
74
+ */
75
+ declare function normalizeExportContact(contact: ExportContact): ExportContact;
76
+ /** A loose shape check for a non-blocking "check this address" hint. */
77
+ declare function looksLikeEmail(value: string): boolean;
78
+
79
+ export { type ExportContact as E, EXPORT_CONTACT_LIMITS as a, type ExportContactDraft as b, exportContactSchema as e, looksLikeEmail as l, normalizeExportContact as n, pickExportContact as p };
package/dist/index.cjs CHANGED
@@ -7692,7 +7692,7 @@ function describeSifaRecord(collection, value) {
7692
7692
  }
7693
7693
 
7694
7694
  // src/index.ts
7695
- var SIFA_SDK_VERSION = "0.19.93";
7695
+ var SIFA_SDK_VERSION = "0.19.95";
7696
7696
 
7697
7697
  exports.ACTIVITY_TIERS = ACTIVITY_TIERS;
7698
7698
  exports.ACTIVITY_VERBS = ACTIVITY_VERBS;
package/dist/index.js CHANGED
@@ -377,7 +377,7 @@ import {
377
377
  roadmapIssueUrl
378
378
  } from "./roadmap/index.js";
379
379
  import { SIFA_REPO_GROUPS, describeSifaRecord, repoGroupForCollection } from "./repo/index.js";
380
- var SIFA_SDK_VERSION = "0.19.93";
380
+ var SIFA_SDK_VERSION = "0.19.95";
381
381
  export {
382
382
  ACTIVITY_TIERS,
383
383
  ACTIVITY_VERBS,
@@ -255,6 +255,13 @@ function mapLocation(profile, s) {
255
255
  ];
256
256
  return candidates.find((c) => Object.keys(c).length > 0);
257
257
  }
258
+ function link(value, s) {
259
+ const t = value?.replace(/\s+/g, "");
260
+ if (!t) return void 0;
261
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(t)) return URL.canParse(t) ? s(t) : void 0;
262
+ const withScheme = `https://${t}`;
263
+ return /^[^\s/]+\.[^\s/]+/.test(t) && URL.canParse(withScheme) ? s(withScheme) : void 0;
264
+ }
258
265
  function text(value, s) {
259
266
  const trimmed = value?.trim();
260
267
  return trimmed ? s(trimmed) : void 0;
@@ -272,7 +279,7 @@ function profileToJsonResume(profile, options = {}) {
272
279
  (account) => compact({
273
280
  network: s(account.platform),
274
281
  username: text(account.label, s),
275
- url: text(account.url, s)
282
+ url: link(account.url, s)
276
283
  })
277
284
  );
278
285
  const basics = compact({
@@ -281,7 +288,7 @@ function profileToJsonResume(profile, options = {}) {
281
288
  image: text(profile.avatar, s),
282
289
  email: text(contact.email, s),
283
290
  phone: text(contact.phone, s),
284
- url: text(profile.website, s) ?? canonical,
291
+ url: link(profile.website, s) ?? canonical,
285
292
  summary: text(profile.about, s),
286
293
  location: withAddress(mapLocation(profile, s), text(contact.address, s)),
287
294
  ...section("profiles", profiles)
@@ -328,7 +335,7 @@ function profileToJsonResume(profile, options = {}) {
328
335
  // `issuingOrg` is the deprecated read-view alias for `authority`; records
329
336
  // indexed before the rename carry only the alias.
330
337
  issuer: orgName(cert.entityName, cert.authority ?? cert.issuingOrg, s),
331
- url: text(cert.credentialUrl, s)
338
+ url: link(cert.credentialUrl, s)
332
339
  })
333
340
  );
334
341
  const publications = sortPublications(filterHidden(profile.publications)).map(
@@ -336,7 +343,7 @@ function profileToJsonResume(profile, options = {}) {
336
343
  name: s(publication.title),
337
344
  publisher: text(publication.publisher, s),
338
345
  releaseDate: toResumeDate(publication.date),
339
- url: text(publication.url, s),
346
+ url: link(publication.url, s),
340
347
  summary: text(publication.description, s)
341
348
  })
342
349
  );
@@ -351,7 +358,7 @@ function profileToJsonResume(profile, options = {}) {
351
358
  (project) => compact({
352
359
  name: s(project.name),
353
360
  description: text(project.description, s),
354
- url: text(project.url, s),
361
+ url: link(project.url, s),
355
362
  startDate: toResumeDate(project.startDate),
356
363
  endDate: toResumeDate(project.endDate)
357
364
  })