@takuhon/core 0.11.0 → 0.13.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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  var $schema = "https://json-schema.org/draft/2020-12/schema";
2
- var $id = "https://takuhon.example/schemas/0.4.0/takuhon.schema.json";
2
+ var $id = "https://takuhon.example/schemas/0.5.0/takuhon.schema.json";
3
3
  var title = "Takuhon Profile";
4
4
  var description = "Portable profile data format consumed by @takuhon/core. The canonical contract for profile content authored as takuhon.json.";
5
5
  var type = "object";
@@ -542,6 +542,74 @@ var $defs = {
542
542
  type: "boolean",
543
543
  "default": false,
544
544
  description: "Opt-in flag for first-party analytics. Default is false to keep takuhon privacy-respecting by default."
545
+ },
546
+ activity: {
547
+ $ref: "#/$defs/ActivitySettings"
548
+ }
549
+ }
550
+ },
551
+ ActivitySettings: {
552
+ type: "object",
553
+ additionalProperties: false,
554
+ description: "Opt-in developer-activity dashboard configuration (GitHub / WakaTime). Only the owner-curated settings live here; secrets are provisioned out of band and the synced metrics are stored in a separate document, never in takuhon.json.",
555
+ properties: {
556
+ enabled: {
557
+ type: "boolean",
558
+ "default": false,
559
+ description: "Master switch. When false (the default), the activity section is not rendered even if a snapshot exists."
560
+ },
561
+ github: {
562
+ type: "object",
563
+ additionalProperties: false,
564
+ required: [
565
+ "username"
566
+ ],
567
+ properties: {
568
+ username: {
569
+ type: "string",
570
+ minLength: 1,
571
+ maxLength: 39,
572
+ description: "GitHub login whose public activity is summarized."
573
+ },
574
+ showLanguages: {
575
+ type: "boolean",
576
+ "default": true
577
+ },
578
+ showContributions: {
579
+ type: "boolean",
580
+ "default": true
581
+ }
582
+ }
583
+ },
584
+ wakatime: {
585
+ type: "object",
586
+ additionalProperties: false,
587
+ required: [
588
+ "username"
589
+ ],
590
+ properties: {
591
+ username: {
592
+ type: "string",
593
+ minLength: 1,
594
+ maxLength: 255,
595
+ description: "WakaTime username whose coding-time stats are summarized."
596
+ },
597
+ showCodingTime: {
598
+ type: "boolean",
599
+ "default": true
600
+ }
601
+ }
602
+ },
603
+ showRank: {
604
+ type: "boolean",
605
+ "default": true,
606
+ description: "Display the derived activity rank / badge."
607
+ },
608
+ refreshHintHours: {
609
+ type: "integer",
610
+ minimum: 1,
611
+ maximum: 168,
612
+ description: "Advisory refresh cadence in hours. The real cadence is how often the sync step (CLI command or scheduled job) runs."
545
613
  }
546
614
  }
547
615
  },
@@ -1664,6 +1732,70 @@ declare const schema: {
1664
1732
  default: boolean;
1665
1733
  description: string;
1666
1734
  };
1735
+ activity: {
1736
+ $ref: string;
1737
+ };
1738
+ };
1739
+ };
1740
+ ActivitySettings: {
1741
+ type: string;
1742
+ additionalProperties: boolean;
1743
+ description: string;
1744
+ properties: {
1745
+ enabled: {
1746
+ type: string;
1747
+ default: boolean;
1748
+ description: string;
1749
+ };
1750
+ github: {
1751
+ type: string;
1752
+ additionalProperties: boolean;
1753
+ required: string[];
1754
+ properties: {
1755
+ username: {
1756
+ type: string;
1757
+ minLength: number;
1758
+ maxLength: number;
1759
+ description: string;
1760
+ };
1761
+ showLanguages: {
1762
+ type: string;
1763
+ default: boolean;
1764
+ };
1765
+ showContributions: {
1766
+ type: string;
1767
+ default: boolean;
1768
+ };
1769
+ };
1770
+ };
1771
+ wakatime: {
1772
+ type: string;
1773
+ additionalProperties: boolean;
1774
+ required: string[];
1775
+ properties: {
1776
+ username: {
1777
+ type: string;
1778
+ minLength: number;
1779
+ maxLength: number;
1780
+ description: string;
1781
+ };
1782
+ showCodingTime: {
1783
+ type: string;
1784
+ default: boolean;
1785
+ };
1786
+ };
1787
+ };
1788
+ showRank: {
1789
+ type: string;
1790
+ default: boolean;
1791
+ description: string;
1792
+ };
1793
+ refreshHintHours: {
1794
+ type: string;
1795
+ minimum: number;
1796
+ maximum: number;
1797
+ description: string;
1798
+ };
1667
1799
  };
1668
1800
  };
1669
1801
  Meta: {
@@ -2515,6 +2647,32 @@ interface Settings {
2515
2647
  enableApi?: boolean;
2516
2648
  /** Opt-in flag for first-party analytics. Default is false. */
2517
2649
  enableAnalytics?: boolean;
2650
+ /** Opt-in developer-activity dashboard configuration (added in 0.5.0). */
2651
+ activity?: ActivitySettings;
2652
+ }
2653
+ /**
2654
+ * Owner-curated configuration for the developer-activity dashboard (GitHub /
2655
+ * WakaTime). Secrets (API tokens) are provisioned out of band and never stored
2656
+ * here; the synced metrics live in a separate document, not in `takuhon.json`.
2657
+ */
2658
+ interface ActivitySettings {
2659
+ /** Master switch; the section is hidden when false (the default). */
2660
+ enabled?: boolean;
2661
+ github?: {
2662
+ /** GitHub login whose public activity is summarized. */
2663
+ username: string;
2664
+ showLanguages?: boolean;
2665
+ showContributions?: boolean;
2666
+ };
2667
+ wakatime?: {
2668
+ /** WakaTime username whose coding-time stats are summarized. */
2669
+ username: string;
2670
+ showCodingTime?: boolean;
2671
+ };
2672
+ /** Display the derived activity rank / badge. */
2673
+ showRank?: boolean;
2674
+ /** Advisory refresh cadence in hours (the real cadence is the sync schedule). */
2675
+ refreshHintHours?: number;
2518
2676
  }
2519
2677
  interface ContentLicenseAttribution {
2520
2678
  name?: string;
@@ -2862,7 +3020,7 @@ interface LocalizedTakuhon {
2862
3020
  * versions whose JSON Schema this package literally bundles, not the full
2863
3021
  * support window seen by end users.
2864
3022
  */
2865
- declare const SUPPORTED_SCHEMA_VERSIONS: readonly ["0.1.0", "0.2.0", "0.3.0", "0.4.0"];
3023
+ declare const SUPPORTED_SCHEMA_VERSIONS: readonly ["0.1.0", "0.2.0", "0.3.0", "0.4.0", "0.5.0"];
2866
3024
  /**
2867
3025
  * A single validation failure.
2868
3026
  *
@@ -3169,9 +3327,9 @@ declare function applyPublicPrivacyFilter<T extends FilterableProfile>(profile:
3169
3327
  *
3170
3328
  * {@link migrateTakuhon} composes a chain of {@link Migration} entries from
3171
3329
  * the registry (`./migrations`) and applies them in order. The registry
3172
- * currently ships the `0.1.0 → 0.2.0`, `0.2.0 → 0.3.0`, and `0.3.0 → 0.4.0`
3173
- * forward migrations; a request whose source has no chain to the target
3174
- * throws {@link MigrationError}.
3330
+ * currently ships the `0.1.0 → 0.2.0`, `0.2.0 → 0.3.0`, `0.3.0 → 0.4.0`, and
3331
+ * `0.4.0 → 0.5.0` forward migrations; a request whose source has no chain to
3332
+ * the target throws {@link MigrationError}.
3175
3333
  *
3176
3334
  * Scope (deliberately narrow, mirroring `export.ts`):
3177
3335
  * - Pure data transform — no I/O, no backup creation, no storage write.
@@ -3240,6 +3398,130 @@ interface Migration<From, To> {
3240
3398
  */
3241
3399
  declare const migrations: readonly Migration<Takuhon, Takuhon>[];
3242
3400
 
3401
+ /**
3402
+ * Developer-activity snapshot: types, a runtime type-guard, and the pure,
3403
+ * network-free transforms that turn raw GitHub / WakaTime figures into the
3404
+ * stored, render-ready shape.
3405
+ *
3406
+ * This module is deliberately dependency-free and platform-independent — it
3407
+ * never performs I/O or network access. Fetching the raw figures from GitHub /
3408
+ * WakaTime lives in a separate, runtime-aware package (`@takuhon/activity`);
3409
+ * core only owns the data shape and the deterministic transforms, so every
3410
+ * runtime renders identical output from the same snapshot.
3411
+ *
3412
+ * The {@link ActivitySnapshot} is NOT part of the canonical `takuhon.json`
3413
+ * document. It is machine-written by a sync step (a CLI command or a scheduled
3414
+ * job), persisted in a sibling document via {@link ActivityStorage}, and read
3415
+ * by the renderer. Keeping it out of the canonical, owner-curated identity
3416
+ * document is intentional: externally-sourced, volatile metrics must not enter
3417
+ * the single source of truth a profile owner hand-maintains.
3418
+ */
3419
+ /** One language's share of analyzed source, derived from byte counts. */
3420
+ interface LanguageBreakdown {
3421
+ /** Language name as reported by the source (e.g. `"TypeScript"`). */
3422
+ name: string;
3423
+ /** Total bytes attributed to this language across analyzed repositories. */
3424
+ bytes: number;
3425
+ /** Share of the analyzed total, 0–100, rounded to one decimal place. */
3426
+ percent: number;
3427
+ }
3428
+ /** A single day in a contribution calendar. */
3429
+ interface ContributionDay {
3430
+ /** ISO-8601 calendar date (`YYYY-MM-DD`). */
3431
+ date: string;
3432
+ /** Contribution count on that day. */
3433
+ count: number;
3434
+ }
3435
+ /** Contribution activity over a window (e.g. the trailing year). */
3436
+ interface ContributionCalendar {
3437
+ /** Total contributions across the window. */
3438
+ total: number;
3439
+ /** Per-day counts, in chronological order. */
3440
+ days: ContributionDay[];
3441
+ }
3442
+ /** Coding time decomposed into whole hours/minutes/seconds plus the raw total. */
3443
+ interface CodingTime {
3444
+ totalSeconds: number;
3445
+ hours: number;
3446
+ minutes: number;
3447
+ seconds: number;
3448
+ }
3449
+ /** Coarse activity tier label, highest (`S`) to lowest (`D`). */
3450
+ type RankTierLabel = 'S' | 'A' | 'B' | 'C' | 'D';
3451
+ /** A derived activity rank: a tier label plus the 0–100 score it came from. */
3452
+ interface RankTier {
3453
+ tier: RankTierLabel;
3454
+ /** Normalized 0–100 activity score the tier was derived from. */
3455
+ score: number;
3456
+ }
3457
+ /**
3458
+ * A point-in-time snapshot of externally-sourced developer-activity metrics
3459
+ * (GitHub languages / contributions, WakaTime coding time, and a derived rank).
3460
+ *
3461
+ * Every metric field is optional so the snapshot degrades gracefully when a
3462
+ * source is unconfigured or temporarily unavailable: the renderer shows what is
3463
+ * present and omits the rest. Only {@link lastSyncedAt} is required so staleness
3464
+ * is always displayable.
3465
+ */
3466
+ interface ActivitySnapshot {
3467
+ /** ISO-8601 timestamp of when this snapshot was produced. */
3468
+ lastSyncedAt: string;
3469
+ languages?: LanguageBreakdown[];
3470
+ contributions?: ContributionCalendar;
3471
+ codingTime?: CodingTime;
3472
+ rank?: RankTier;
3473
+ }
3474
+ /**
3475
+ * Validate that an unknown value (e.g. one read back from storage) is a
3476
+ * well-formed {@link ActivitySnapshot}. A lightweight runtime guard is
3477
+ * sufficient here — the snapshot is machine-written and self-owned, so it does
3478
+ * not warrant a full JSON Schema validator like `takuhon.json`. A malformed or
3479
+ * truncated snapshot fails the guard and the renderer treats it as absent.
3480
+ */
3481
+ declare function isActivitySnapshot(value: unknown): value is ActivitySnapshot;
3482
+ /**
3483
+ * Turn per-language byte counts (as GitHub's `repos/.../languages` reports) into
3484
+ * sorted percentage breakdowns. Languages with zero bytes are dropped; the
3485
+ * result is sorted by bytes descending, ties broken by name, so the output is
3486
+ * deterministic. Percentages are rounded to one decimal place and need not sum
3487
+ * to exactly 100. Returns `[]` when there is nothing to attribute.
3488
+ */
3489
+ declare function computeLanguagePercentages(bytesByLanguage: Readonly<Record<string, number>>): LanguageBreakdown[];
3490
+ /** Decompose a total number of seconds into whole h/m/s (negatives clamp to 0). */
3491
+ declare function formatCodingTime(totalSeconds: number): CodingTime;
3492
+ /** Inputs to {@link deriveRankTier}; any field may be absent. */
3493
+ interface RankInput {
3494
+ /** Contributions in the trailing window (e.g. the past year). */
3495
+ contributions?: number;
3496
+ /** Total coding time, in seconds. */
3497
+ codingSeconds?: number;
3498
+ }
3499
+ /** Contribution count that saturates the contribution sub-score at 100. */
3500
+ declare const RANK_FULL_CONTRIBUTIONS = 2000;
3501
+ /** Coding hours that saturate the coding-time sub-score at 100. */
3502
+ declare const RANK_FULL_CODING_HOURS = 2000;
3503
+ /**
3504
+ * Inclusive lower bounds for each tier on the 0–100 score (anything below `C`
3505
+ * is `D`). These are a deliberately generic, transparent default — not tuned to
3506
+ * any individual — and may be revisited.
3507
+ */
3508
+ declare const RANK_TIER_THRESHOLDS: {
3509
+ readonly S: 80;
3510
+ readonly A: 60;
3511
+ readonly B: 40;
3512
+ readonly C: 20;
3513
+ };
3514
+ /**
3515
+ * Derive a coarse activity {@link RankTier} from the available signals. Each
3516
+ * present signal is mapped to a 0–100 sub-score via linear saturation
3517
+ * ({@link RANK_FULL_CONTRIBUTIONS} / {@link RANK_FULL_CODING_HOURS} reach 100);
3518
+ * the final score is the mean of the present sub-scores, so a profile is not
3519
+ * penalized for leaving a source unconfigured. With no signals the score is 0
3520
+ * (`D`). The function is deterministic — no clock or randomness — so renders are
3521
+ * reproducible.
3522
+ */
3523
+ declare function deriveRankTier(input: RankInput): RankTier;
3524
+
3243
3525
  /**
3244
3526
  * Persistence contracts for takuhon profile documents and binary assets.
3245
3527
  *
@@ -3323,8 +3605,14 @@ interface AssetRecord {
3323
3605
  /**
3324
3606
  * Caller hints for {@link TakuhonAssetStorage.putAsset}. Both fields are
3325
3607
  * optional; when omitted, the adapter falls back to the `File` / `Blob`
3326
- * metadata. Adapters are responsible for magic-byte verification, EXIF
3327
- * stripping, and dimension limits — callers must not pre-process.
3608
+ * metadata.
3609
+ *
3610
+ * The caller (`@takuhon/api`'s admin app) verifies the magic bytes, enforces
3611
+ * the size / dimension / frame limits, and strips metadata (EXIF / IPTC / XMP /
3612
+ * color profile) before calling `putAsset`, using the shared `@takuhon/core`
3613
+ * image helpers, so every adapter applies the same checks. An adapter therefore
3614
+ * persists the already-validated, already-stripped bytes verbatim and is
3615
+ * responsible only for storage and key/URL generation.
3328
3616
  */
3329
3617
  interface AssetOptions {
3330
3618
  filename?: string;
@@ -3346,6 +3634,27 @@ interface TakuhonAssetStorage {
3346
3634
  deleteAsset(assetId: string): Promise<void>;
3347
3635
  listAssets(): Promise<AssetRecord[]>;
3348
3636
  }
3637
+ /**
3638
+ * Persistence contract for the developer-activity snapshot (see
3639
+ * {@link ActivitySnapshot}). Like {@link TakuhonAssetStorage}, this is a
3640
+ * separate, optional interface so deployments that don't enable the activity
3641
+ * feature can omit it entirely.
3642
+ *
3643
+ * Unlike {@link TakuhonStorage}, there is no optimistic-locking `version`: the
3644
+ * snapshot is machine-written by a sync step (a CLI command or a scheduled job)
3645
+ * with last-writer-wins semantics and is never concurrently hand-edited.
3646
+ */
3647
+ interface ActivityStorage {
3648
+ /**
3649
+ * Read the current activity snapshot, or `null` when none has been synced
3650
+ * yet. Returns `null` rather than throwing {@link NotFoundError}: an absent
3651
+ * snapshot is the normal opt-out / pre-sync state, and the renderer omits the
3652
+ * activity section gracefully when it is `null`.
3653
+ */
3654
+ getActivitySnapshot(): Promise<ActivitySnapshot | null>;
3655
+ /** Replace the stored snapshot (last-writer-wins; no precondition). */
3656
+ saveActivitySnapshot(snapshot: ActivitySnapshot): Promise<void>;
3657
+ }
3349
3658
  /**
3350
3659
  * Base class for errors thrown by storage adapters. Catch this to handle
3351
3660
  * any storage-layer failure uniformly; check `instanceof` of a subclass
@@ -3378,6 +3687,182 @@ declare class ConflictError extends StorageError {
3378
3687
  });
3379
3688
  }
3380
3689
 
3690
+ /**
3691
+ * Self-owned inline SVG rendering of an {@link ActivitySnapshot}.
3692
+ *
3693
+ * The dashboard is drawn from the stored snapshot only — no external badge
3694
+ * image, no network access — so a page embedding it keeps a strict
3695
+ * `img-src 'self'` Content-Security-Policy. The output is a deterministic pure
3696
+ * string: no clock, no randomness, no locale-dependent formatting — the same
3697
+ * snapshot always renders the same markup, so both rendering surfaces (the
3698
+ * static HTML export and the React profile) and their tests stay in lockstep.
3699
+ *
3700
+ * Every snapshot-derived string (language names, dates) is XML-escaped before
3701
+ * it reaches the markup: language names come from an external API and are
3702
+ * treated as untrusted. An empty snapshot (no metric fields) renders to `''`
3703
+ * so callers can omit the section with a truthiness check.
3704
+ */
3705
+
3706
+ /**
3707
+ * Render the activity snapshot as a self-contained `<svg>` string, or `''`
3708
+ * when the snapshot carries no metric data (so callers can omit the section).
3709
+ * Sections render independently — whatever the snapshot holds is shown, the
3710
+ * rest is left out — and `lastSyncedAt`'s date is always stamped in the footer
3711
+ * so staleness stays visible.
3712
+ */
3713
+ declare function renderActivitySvg(snapshot: ActivitySnapshot): string;
3714
+
3715
+ /**
3716
+ * Résumé / CV projection of a profile.
3717
+ *
3718
+ * {@link deriveCv} turns one locale-resolved {@link LocalizedTakuhon} into a
3719
+ * {@link CvDocument}: a header plus the CV-relevant sections in a fixed,
3720
+ * résumé-conventional order. It is a pure, deterministic selection-and-projection
3721
+ * — no clock, no randomness, no I/O — so the static export ({@link
3722
+ * import('@takuhon/cli')}) and a React `CvView` render identical output from the
3723
+ * same input.
3724
+ *
3725
+ * A CV is a curated *subset* of the profile: the web-page-specific sections
3726
+ * (links, recommendations, the activity dashboard, test scores) are omitted,
3727
+ * and the rest appear in the order a résumé conventionally uses. Entry order
3728
+ * within a section is preserved from the input (already normalized by `order`
3729
+ * upstream), so the owner's `order` field controls it — `deriveCv` never
3730
+ * re-sorts. Empty sections are dropped so the renderer can iterate without
3731
+ * emptiness checks.
3732
+ *
3733
+ * The input must already be privacy-filtered (`applyPublicPrivacyFilter`) by the
3734
+ * caller, exactly as the public render path is: `deriveCv` does not strip
3735
+ * anything itself, it only projects what it is given.
3736
+ */
3737
+
3738
+ /** The header block of a CV: identity and contact, drawn from `profile` + `contact`. */
3739
+ interface CvHeader {
3740
+ displayName: string;
3741
+ tagline?: string;
3742
+ /** Human-readable location (the address `display` string), if present. */
3743
+ location?: string;
3744
+ bio?: string;
3745
+ /** Email, only when the profile exposed it (privacy filter already applied). */
3746
+ email?: string;
3747
+ formUrl?: string;
3748
+ }
3749
+ /**
3750
+ * One CV section, discriminated by `kind`, carrying the locale-resolved entries
3751
+ * for that section. The entry types are reused verbatim from `LocalizedTakuhon`.
3752
+ */
3753
+ type CvSection = {
3754
+ kind: 'experience';
3755
+ entries: LocalizedCareer[];
3756
+ } | {
3757
+ kind: 'education';
3758
+ entries: LocalizedEducation[];
3759
+ } | {
3760
+ kind: 'skills';
3761
+ entries: Skill[];
3762
+ } | {
3763
+ kind: 'certifications';
3764
+ entries: LocalizedCertification[];
3765
+ } | {
3766
+ kind: 'publications';
3767
+ entries: LocalizedPublication[];
3768
+ } | {
3769
+ kind: 'honors';
3770
+ entries: LocalizedHonor[];
3771
+ } | {
3772
+ kind: 'courses';
3773
+ entries: LocalizedCourse[];
3774
+ } | {
3775
+ kind: 'patents';
3776
+ entries: LocalizedPatent[];
3777
+ } | {
3778
+ kind: 'languages';
3779
+ entries: LocalizedLanguage[];
3780
+ } | {
3781
+ kind: 'volunteering';
3782
+ entries: LocalizedVolunteering[];
3783
+ } | {
3784
+ kind: 'memberships';
3785
+ entries: LocalizedMembership[];
3786
+ };
3787
+ /** Every `CvSection` discriminant, in the fixed résumé order `deriveCv` emits. */
3788
+ type CvSectionKind = CvSection['kind'];
3789
+ /** A résumé/CV view derived from a profile: a header plus ordered sections. */
3790
+ interface CvDocument {
3791
+ /** The locale this CV was resolved at (mirrors `LocalizedTakuhon.resolvedLocale`). */
3792
+ resolvedLocale: string;
3793
+ header: CvHeader;
3794
+ /** Non-empty sections only, in the fixed CV order. */
3795
+ sections: CvSection[];
3796
+ }
3797
+ /**
3798
+ * Project a locale-resolved profile into a {@link CvDocument}. Sections appear
3799
+ * in a fixed résumé-conventional order and empty ones are omitted; entry order
3800
+ * within each section is preserved from the input. Pure and deterministic.
3801
+ */
3802
+ declare function deriveCv(localized: LocalizedTakuhon): CvDocument;
3803
+
3804
+ /**
3805
+ * Image-asset helpers for uploaded media (avatars, project images): magic-byte
3806
+ * type detection, header-only dimension / frame reading, and metadata stripping
3807
+ * (EXIF / IPTC / XMP / color profile) for the four MIME types takuhon accepts.
3808
+ *
3809
+ * Every function parses only the container structure — it never decodes pixel
3810
+ * data — so the module is tiny, dependency-free, and runs identically on
3811
+ * Cloudflare Workers, Node, and in the browser. This is the deliberate
3812
+ * "no codec, no re-encode" approach: `security.md` §4 requires metadata removal
3813
+ * for privacy (EXIF GPS / device data on avatars) but leaves resizing to the UI
3814
+ * (§4.3 "aspect ratio cropped UI-side"), so no image codec is needed.
3815
+ *
3816
+ * AVIF is intentionally not handled yet — its ISO-BMFF metadata layout is more
3817
+ * involved — so {@link detectImageMime} returns `null` for it and uploads of it
3818
+ * are rejected until a later phase adds support.
3819
+ *
3820
+ * The functions assume well-formed input only as far as security requires:
3821
+ * {@link detectImageMime} authenticates the type from the leading bytes (so a
3822
+ * spoofed `Content-Type` cannot smuggle, say, an SVG), {@link readImageInfo}
3823
+ * returns `null` when it cannot find the dimensions, and
3824
+ * {@link stripImageMetadata} preserves the pixel data while dropping metadata
3825
+ * segments. A truncated or malformed file is the uploader's problem — the bytes
3826
+ * are stored and served verbatim, never decoded server-side.
3827
+ */
3828
+ /** MIME types accepted for uploaded images (`security.md` §4.1, minus AVIF). */
3829
+ declare const ACCEPTED_IMAGE_MIME_TYPES: readonly ["image/jpeg", "image/png", "image/webp", "image/gif"];
3830
+ type AcceptedImageMime = (typeof ACCEPTED_IMAGE_MIME_TYPES)[number];
3831
+ /** Maximum accepted upload size, in bytes (`security.md` §4.3). */
3832
+ declare const MAX_IMAGE_BYTES: number;
3833
+ /** Maximum accepted width or height, in pixels (`security.md` §4.3). */
3834
+ declare const MAX_IMAGE_DIMENSION = 4096;
3835
+ /** Maximum accepted animation frame count (`security.md` §4.4). */
3836
+ declare const MAX_IMAGE_FRAMES = 100;
3837
+ /** Canonical file extension per accepted MIME, used in object keys. */
3838
+ declare const IMAGE_EXTENSIONS: Record<AcceptedImageMime, string>;
3839
+ /** Dimensions and animation frame count read from an image's container header. */
3840
+ interface ImageInfo {
3841
+ width: number;
3842
+ height: number;
3843
+ /** 1 for a still image; the frame count for an animated GIF / WebP / APNG. */
3844
+ frames: number;
3845
+ }
3846
+ /**
3847
+ * Identify an accepted image type from its leading bytes, independent of any
3848
+ * declared `Content-Type` (`security.md` §4.2). Returns `null` for unrecognized
3849
+ * or unsupported (e.g. AVIF, SVG) input.
3850
+ */
3851
+ declare function detectImageMime(bytes: Uint8Array): AcceptedImageMime | null;
3852
+ /**
3853
+ * Read an image's dimensions and animation frame count from its container
3854
+ * header alone (no pixel decode). Returns `null` when the structure cannot be
3855
+ * parsed. `mime` must come from {@link detectImageMime}.
3856
+ */
3857
+ declare function readImageInfo(bytes: Uint8Array, mime: AcceptedImageMime): ImageInfo | null;
3858
+ /**
3859
+ * Remove metadata (EXIF / IPTC / XMP / embedded color profile / comments) from
3860
+ * an image while preserving its pixel data, by editing only the container
3861
+ * structure (`security.md` §4.5). `mime` must come from {@link detectImageMime}.
3862
+ * Returns the original bytes unchanged when there is no metadata to remove.
3863
+ */
3864
+ declare function stripImageMetadata(bytes: Uint8Array, mime: AcceptedImageMime): Uint8Array;
3865
+
3381
3866
  /**
3382
3867
  * @takuhon/core — canonical JSON Schema, hand-written TypeScript types,
3383
3868
  * Ajv-backed validation, document normalization, and locale resolution for
@@ -3414,6 +3899,6 @@ declare class ConflictError extends StorageError {
3414
3899
  * A takuhon profile document's `schemaVersion` field must be migrate-compatible
3415
3900
  * with this version. See operational-lifecycle docs for the migration policy.
3416
3901
  */
3417
- declare const SCHEMA_VERSION = "0.4.0";
3902
+ declare const SCHEMA_VERSION = "0.5.0";
3418
3903
 
3419
- export { type Address, type AssetOptions, type AssetRecord, type Avatar, type Career, type Certification, ConflictError, type Contact, type ContentLicense, type ContentLicenseAttribution, type Course, type Education, type ExportOptions, type ExportedTakuhon, type Honor, ImportError, type Iso3166Alpha2, type IsoDateTime, type Language, type LanguageProficiency, type Link, type LinkBuiltin, type LinkCustom, type LinkType, type LocaleTag, type LocalizedAddress, type LocalizedAvatar, type LocalizedBody, type LocalizedCareer, type LocalizedCertification, type LocalizedCourse, type LocalizedEducation, type LocalizedHonor, type LocalizedLanguage, type LocalizedLink, type LocalizedLinkBuiltin, type LocalizedLinkCustom, type LocalizedMembership, type LocalizedPatent, type LocalizedProfile, type LocalizedProject, type LocalizedPublication, type LocalizedRecommendation, type LocalizedRecommendationAuthor, type LocalizedTakuhon, type LocalizedTestScore, type LocalizedTitle, type LocalizedVolunteering, type Membership, type Meta, type MetaPrivacy, type Migration, MigrationError, type NormalizedTakuhon, NotFoundError, type Patent, type PatentStatus, type Profile, type Project, type Publication, type Recommendation, type RecommendationAuthor, SCHEMA_VERSION, SUPPORTED_SCHEMA_VERSIONS, type Schema, type Settings, type Skill, type Slug, StorageError, type Takuhon, type TakuhonAssetStorage, type TakuhonStorage, type TestScore, type ValidationError, type ValidationResult, type Volunteering, type YearMonth, applyPublicPrivacyFilter, exportTakuhon, generateJsonLd, generatePersonJsonLd, generateProfilePageJsonLd, importTakuhon, migrateTakuhon, migrations, normalize, resolveLocale, schema, validate };
3904
+ export { ACCEPTED_IMAGE_MIME_TYPES, type AcceptedImageMime, type ActivitySettings, type ActivitySnapshot, type ActivityStorage, type Address, type AssetOptions, type AssetRecord, type Avatar, type Career, type Certification, type CodingTime, ConflictError, type Contact, type ContentLicense, type ContentLicenseAttribution, type ContributionCalendar, type ContributionDay, type Course, type CvDocument, type CvHeader, type CvSection, type CvSectionKind, type Education, type ExportOptions, type ExportedTakuhon, type Honor, IMAGE_EXTENSIONS, type ImageInfo, ImportError, type Iso3166Alpha2, type IsoDateTime, type Language, type LanguageBreakdown, type LanguageProficiency, type Link, type LinkBuiltin, type LinkCustom, type LinkType, type LocaleTag, type LocalizedAddress, type LocalizedAvatar, type LocalizedBody, type LocalizedCareer, type LocalizedCertification, type LocalizedCourse, type LocalizedEducation, type LocalizedHonor, type LocalizedLanguage, type LocalizedLink, type LocalizedLinkBuiltin, type LocalizedLinkCustom, type LocalizedMembership, type LocalizedPatent, type LocalizedProfile, type LocalizedProject, type LocalizedPublication, type LocalizedRecommendation, type LocalizedRecommendationAuthor, type LocalizedTakuhon, type LocalizedTestScore, type LocalizedTitle, type LocalizedVolunteering, MAX_IMAGE_BYTES, MAX_IMAGE_DIMENSION, MAX_IMAGE_FRAMES, type Membership, type Meta, type MetaPrivacy, type Migration, MigrationError, type NormalizedTakuhon, NotFoundError, type Patent, type PatentStatus, type Profile, type Project, type Publication, RANK_FULL_CODING_HOURS, RANK_FULL_CONTRIBUTIONS, RANK_TIER_THRESHOLDS, type RankInput, type RankTier, type RankTierLabel, type Recommendation, type RecommendationAuthor, SCHEMA_VERSION, SUPPORTED_SCHEMA_VERSIONS, type Schema, type Settings, type Skill, type Slug, StorageError, type Takuhon, type TakuhonAssetStorage, type TakuhonStorage, type TestScore, type ValidationError, type ValidationResult, type Volunteering, type YearMonth, applyPublicPrivacyFilter, computeLanguagePercentages, deriveCv, deriveRankTier, detectImageMime, exportTakuhon, formatCodingTime, generateJsonLd, generatePersonJsonLd, generateProfilePageJsonLd, importTakuhon, isActivitySnapshot, migrateTakuhon, migrations, normalize, readImageInfo, renderActivitySvg, resolveLocale, schema, stripImageMetadata, validate };