@jwilleke/ngdpbase-types 5.39.0 → 5.41.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.
Files changed (99) hide show
  1. package/lib/packages/ngdpbase-types/index.d.ts +1 -1
  2. package/lib/src/context/ApiContext.d.ts +3 -2
  3. package/lib/src/context/ApiContext.js +4 -2
  4. package/lib/src/ice/fileHandling.d.ts +6 -1
  5. package/lib/src/ice/givenName.d.ts +19 -0
  6. package/lib/src/ice/runExport.d.ts +41 -0
  7. package/lib/src/ice/runImport.d.ts +37 -8
  8. package/lib/src/ice/shims/csv.d.ts +24 -0
  9. package/lib/src/ice/shims/export.d.ts +23 -0
  10. package/lib/src/ice/shims/index.d.ts +3 -1
  11. package/lib/src/ice/shims/svg.d.ts +27 -0
  12. package/lib/src/ice/shims/takeout.d.ts +83 -0
  13. package/lib/src/ice/types.d.ts +301 -5
  14. package/lib/src/ice/vet.d.ts +19 -0
  15. package/lib/src/managers/AttachmentManager.d.ts +100 -36
  16. package/lib/src/managers/AuthManager.d.ts +152 -12
  17. package/lib/src/managers/AuthManager.js +3830 -0
  18. package/lib/src/managers/BackgroundJobManager.d.ts +25 -0
  19. package/lib/src/managers/BackupManager.d.ts +24 -0
  20. package/lib/src/managers/BackupManager.js +48 -1
  21. package/lib/src/managers/ExportManager.d.ts +16 -0
  22. package/lib/src/managers/ExportManager.js +25 -0
  23. package/lib/src/managers/FileManager.d.ts +182 -0
  24. package/lib/src/managers/ImportManager.d.ts +279 -16
  25. package/lib/src/managers/MediaManager.d.ts +2 -0
  26. package/lib/src/managers/PageManager.d.ts +53 -0
  27. package/lib/src/managers/SearchManager.d.ts +4 -0
  28. package/lib/src/managers/SharedFolderManager.d.ts +320 -0
  29. package/lib/src/managers/ValidationManager.d.ts +5 -2
  30. package/lib/src/middleware/csrf.js +99 -0
  31. package/lib/src/providers/AgentTokenAuthProvider.js +46 -0
  32. package/lib/src/providers/AuthentikBearerAuthProvider.js +176 -0
  33. package/lib/src/providers/BaseCredentialsProvider.js +21 -0
  34. package/lib/src/providers/BaseMediaProvider.d.ts +4 -0
  35. package/lib/src/providers/BasePageProvider.d.ts +8 -0
  36. package/lib/src/providers/CloudflareAccessAuthProvider.js +160 -0
  37. package/lib/src/providers/FileCredentialsProvider.js +188 -0
  38. package/lib/src/providers/FileSystemMediaProvider.d.ts +6 -0
  39. package/lib/src/providers/MagicLinkAuthProvider.js +333 -0
  40. package/lib/src/providers/OidcAuthProvider.js +350 -0
  41. package/lib/src/providers/OidcBearerAuthProvider.js +27 -0
  42. package/lib/src/providers/PasskeyAuthProvider.js +147 -0
  43. package/lib/src/providers/PasswordAuthProvider.js +44 -0
  44. package/lib/src/security/PolicyDecisionPoint.d.ts +3 -2
  45. package/lib/src/security/pageActions.js +43 -0
  46. package/lib/src/security/permissions.generated.d.ts +7 -3
  47. package/lib/src/types/File.d.ts +90 -0
  48. package/lib/src/types/Provider.d.ts +15 -0
  49. package/lib/src/types/express.d.ts +7 -1
  50. package/lib/src/types/guards.js +366 -0
  51. package/lib/src/types/linkedom.d.ts +2 -0
  52. package/lib/src/utils/atomicWrite.js +129 -0
  53. package/lib/src/utils/auditEventNames.generated.d.ts +12 -0
  54. package/lib/src/utils/auditEventNames.generated.js +12 -0
  55. package/lib/src/utils/auditEvents.d.ts +15 -0
  56. package/lib/src/utils/auditEvents.js +17 -1
  57. package/lib/src/utils/bip39English.js +2 -0
  58. package/lib/src/utils/browserDescription.js +28 -0
  59. package/lib/src/utils/delegation.js +79 -0
  60. package/lib/src/utils/deriveUsername.js +38 -0
  61. package/lib/src/utils/emailForLog.js +28 -0
  62. package/lib/src/utils/fileMigrationPlan.d.ts +81 -0
  63. package/lib/src/utils/instanceEnvBackup.d.ts +6 -0
  64. package/lib/src/utils/instanceEnvBackup.js +13 -0
  65. package/lib/src/utils/ipLocation.js +106 -0
  66. package/lib/src/utils/magicLinkDeviceState.js +97 -0
  67. package/lib/src/utils/pageUrl.d.ts +1 -0
  68. package/lib/src/utils/passwordHash.js +150 -0
  69. package/lib/src/utils/permissionCatalog.js +102 -0
  70. package/lib/src/utils/privateStoreAccess.d.ts +25 -0
  71. package/lib/src/utils/privateStoreAccess.js +22 -1
  72. package/lib/src/utils/privateStoreCatalogs.js +57 -0
  73. package/lib/src/utils/privateStoreCrypto.js +266 -0
  74. package/lib/src/utils/privateStoreDoor.d.ts +10 -0
  75. package/lib/src/utils/privateStoreDoor.js +298 -0
  76. package/lib/src/utils/privateStoreImport.d.ts +0 -10
  77. package/lib/src/utils/privateStoreMeta.js +123 -0
  78. package/lib/src/utils/privateStorePath.d.ts +37 -1
  79. package/lib/src/utils/privateStorePath.js +60 -3
  80. package/lib/src/utils/privateStoreTakeout.js +149 -0
  81. package/lib/src/utils/privateStoreUnlock.d.ts +3 -0
  82. package/lib/src/utils/privateStoreUnlock.js +499 -0
  83. package/lib/src/utils/recoveryWordsFile.d.ts +54 -0
  84. package/lib/src/utils/safeRedirect.js +55 -0
  85. package/lib/src/utils/sessionAccount.js +55 -0
  86. package/lib/src/utils/sessionEnd.d.ts +1 -0
  87. package/lib/src/utils/sessionEnd.js +42 -0
  88. package/lib/src/utils/sessionGeneration.js +29 -0
  89. package/lib/src/utils/sessionIdle.d.ts +4 -0
  90. package/lib/src/utils/sessionIdle.js +72 -0
  91. package/lib/src/utils/sessionLimits.js +72 -0
  92. package/lib/src/utils/sessionSecurity.js +122 -0
  93. package/lib/src/utils/shareLockbox.js +101 -0
  94. package/lib/src/utils/sharedFolderAccess.d.ts +57 -0
  95. package/lib/src/utils/sharedFolderAccess.js +80 -0
  96. package/lib/src/utils/userCreateError.js +82 -0
  97. package/lib/src/utils/username.d.ts +2 -0
  98. package/lib/src/utils/username.js +105 -0
  99. package/package.json +2 -1
@@ -0,0 +1,2 @@
1
+ /** BIP-0039 English wordlist (official 2048 words). */
2
+ export const BIP39_ENGLISH = ['abandon', 'ability', 'able', 'about', 'above', 'absent', 'absorb', 'abstract', 'absurd', 'abuse', 'access', 'accident', 'account', 'accuse', 'achieve', 'acid', 'acoustic', 'acquire', 'across', 'act', 'action', 'actor', 'actress', 'actual', 'adapt', 'add', 'addict', 'address', 'adjust', 'admit', 'adult', 'advance', 'advice', 'aerobic', 'affair', 'afford', 'afraid', 'again', 'age', 'agent', 'agree', 'ahead', 'aim', 'air', 'airport', 'aisle', 'alarm', 'album', 'alcohol', 'alert', 'alien', 'all', 'alley', 'allow', 'almost', 'alone', 'alpha', 'already', 'also', 'alter', 'always', 'amateur', 'amazing', 'among', 'amount', 'amused', 'analyst', 'anchor', 'ancient', 'anger', 'angle', 'angry', 'animal', 'ankle', 'announce', 'annual', 'another', 'answer', 'antenna', 'antique', 'anxiety', 'any', 'apart', 'apology', 'appear', 'apple', 'approve', 'april', 'arch', 'arctic', 'area', 'arena', 'argue', 'arm', 'armed', 'armor', 'army', 'around', 'arrange', 'arrest', 'arrive', 'arrow', 'art', 'artefact', 'artist', 'artwork', 'ask', 'aspect', 'assault', 'asset', 'assist', 'assume', 'asthma', 'athlete', 'atom', 'attack', 'attend', 'attitude', 'attract', 'auction', 'audit', 'august', 'aunt', 'author', 'auto', 'autumn', 'average', 'avocado', 'avoid', 'awake', 'aware', 'away', 'awesome', 'awful', 'awkward', 'axis', 'baby', 'bachelor', 'bacon', 'badge', 'bag', 'balance', 'balcony', 'ball', 'bamboo', 'banana', 'banner', 'bar', 'barely', 'bargain', 'barrel', 'base', 'basic', 'basket', 'battle', 'beach', 'bean', 'beauty', 'because', 'become', 'beef', 'before', 'begin', 'behave', 'behind', 'believe', 'below', 'belt', 'bench', 'benefit', 'best', 'betray', 'better', 'between', 'beyond', 'bicycle', 'bid', 'bike', 'bind', 'biology', 'bird', 'birth', 'bitter', 'black', 'blade', 'blame', 'blanket', 'blast', 'bleak', 'bless', 'blind', 'blood', 'blossom', 'blouse', 'blue', 'blur', 'blush', 'board', 'boat', 'body', 'boil', 'bomb', 'bone', 'bonus', 'book', 'boost', 'border', 'boring', 'borrow', 'boss', 'bottom', 'bounce', 'box', 'boy', 'bracket', 'brain', 'brand', 'brass', 'brave', 'bread', 'breeze', 'brick', 'bridge', 'brief', 'bright', 'bring', 'brisk', 'broccoli', 'broken', 'bronze', 'broom', 'brother', 'brown', 'brush', 'bubble', 'buddy', 'budget', 'buffalo', 'build', 'bulb', 'bulk', 'bullet', 'bundle', 'bunker', 'burden', 'burger', 'burst', 'bus', 'business', 'busy', 'butter', 'buyer', 'buzz', 'cabbage', 'cabin', 'cable', 'cactus', 'cage', 'cake', 'call', 'calm', 'camera', 'camp', 'can', 'canal', 'cancel', 'candy', 'cannon', 'canoe', 'canvas', 'canyon', 'capable', 'capital', 'captain', 'car', 'carbon', 'card', 'cargo', 'carpet', 'carry', 'cart', 'case', 'cash', 'casino', 'castle', 'casual', 'cat', 'catalog', 'catch', 'category', 'cattle', 'caught', 'cause', 'caution', 'cave', 'ceiling', 'celery', 'cement', 'census', 'century', 'cereal', 'certain', 'chair', 'chalk', 'champion', 'change', 'chaos', 'chapter', 'charge', 'chase', 'chat', 'cheap', 'check', 'cheese', 'chef', 'cherry', 'chest', 'chicken', 'chief', 'child', 'chimney', 'choice', 'choose', 'chronic', 'chuckle', 'chunk', 'churn', 'cigar', 'cinnamon', 'circle', 'citizen', 'city', 'civil', 'claim', 'clap', 'clarify', 'claw', 'clay', 'clean', 'clerk', 'clever', 'click', 'client', 'cliff', 'climb', 'clinic', 'clip', 'clock', 'clog', 'close', 'cloth', 'cloud', 'clown', 'club', 'clump', 'cluster', 'clutch', 'coach', 'coast', 'coconut', 'code', 'coffee', 'coil', 'coin', 'collect', 'color', 'column', 'combine', 'come', 'comfort', 'comic', 'common', 'company', 'concert', 'conduct', 'confirm', 'congress', 'connect', 'consider', 'control', 'convince', 'cook', 'cool', 'copper', 'copy', 'coral', 'core', 'corn', 'correct', 'cost', 'cotton', 'couch', 'country', 'couple', 'course', 'cousin', 'cover', 'coyote', 'crack', 'cradle', 'craft', 'cram', 'crane', 'crash', 'crater', 'crawl', 'crazy', 'cream', 'credit', 'creek', 'crew', 'cricket', 'crime', 'crisp', 'critic', 'crop', 'cross', 'crouch', 'crowd', 'crucial', 'cruel', 'cruise', 'crumble', 'crunch', 'crush', 'cry', 'crystal', 'cube', 'culture', 'cup', 'cupboard', 'curious', 'current', 'curtain', 'curve', 'cushion', 'custom', 'cute', 'cycle', 'dad', 'damage', 'damp', 'dance', 'danger', 'daring', 'dash', 'daughter', 'dawn', 'day', 'deal', 'debate', 'debris', 'decade', 'december', 'decide', 'decline', 'decorate', 'decrease', 'deer', 'defense', 'define', 'defy', 'degree', 'delay', 'deliver', 'demand', 'demise', 'denial', 'dentist', 'deny', 'depart', 'depend', 'deposit', 'depth', 'deputy', 'derive', 'describe', 'desert', 'design', 'desk', 'despair', 'destroy', 'detail', 'detect', 'develop', 'device', 'devote', 'diagram', 'dial', 'diamond', 'diary', 'dice', 'diesel', 'diet', 'differ', 'digital', 'dignity', 'dilemma', 'dinner', 'dinosaur', 'direct', 'dirt', 'disagree', 'discover', 'disease', 'dish', 'dismiss', 'disorder', 'display', 'distance', 'divert', 'divide', 'divorce', 'dizzy', 'doctor', 'document', 'dog', 'doll', 'dolphin', 'domain', 'donate', 'donkey', 'donor', 'door', 'dose', 'double', 'dove', 'draft', 'dragon', 'drama', 'drastic', 'draw', 'dream', 'dress', 'drift', 'drill', 'drink', 'drip', 'drive', 'drop', 'drum', 'dry', 'duck', 'dumb', 'dune', 'during', 'dust', 'dutch', 'duty', 'dwarf', 'dynamic', 'eager', 'eagle', 'early', 'earn', 'earth', 'easily', 'east', 'easy', 'echo', 'ecology', 'economy', 'edge', 'edit', 'educate', 'effort', 'egg', 'eight', 'either', 'elbow', 'elder', 'electric', 'elegant', 'element', 'elephant', 'elevator', 'elite', 'else', 'embark', 'embody', 'embrace', 'emerge', 'emotion', 'employ', 'empower', 'empty', 'enable', 'enact', 'end', 'endless', 'endorse', 'enemy', 'energy', 'enforce', 'engage', 'engine', 'enhance', 'enjoy', 'enlist', 'enough', 'enrich', 'enroll', 'ensure', 'enter', 'entire', 'entry', 'envelope', 'episode', 'equal', 'equip', 'era', 'erase', 'erode', 'erosion', 'error', 'erupt', 'escape', 'essay', 'essence', 'estate', 'eternal', 'ethics', 'evidence', 'evil', 'evoke', 'evolve', 'exact', 'example', 'excess', 'exchange', 'excite', 'exclude', 'excuse', 'execute', 'exercise', 'exhaust', 'exhibit', 'exile', 'exist', 'exit', 'exotic', 'expand', 'expect', 'expire', 'explain', 'expose', 'express', 'extend', 'extra', 'eye', 'eyebrow', 'fabric', 'face', 'faculty', 'fade', 'faint', 'faith', 'fall', 'false', 'fame', 'family', 'famous', 'fan', 'fancy', 'fantasy', 'farm', 'fashion', 'fat', 'fatal', 'father', 'fatigue', 'fault', 'favorite', 'feature', 'february', 'federal', 'fee', 'feed', 'feel', 'female', 'fence', 'festival', 'fetch', 'fever', 'few', 'fiber', 'fiction', 'field', 'figure', 'file', 'film', 'filter', 'final', 'find', 'fine', 'finger', 'finish', 'fire', 'firm', 'first', 'fiscal', 'fish', 'fit', 'fitness', 'fix', 'flag', 'flame', 'flash', 'flat', 'flavor', 'flee', 'flight', 'flip', 'float', 'flock', 'floor', 'flower', 'fluid', 'flush', 'fly', 'foam', 'focus', 'fog', 'foil', 'fold', 'follow', 'food', 'foot', 'force', 'forest', 'forget', 'fork', 'fortune', 'forum', 'forward', 'fossil', 'foster', 'found', 'fox', 'fragile', 'frame', 'frequent', 'fresh', 'friend', 'fringe', 'frog', 'front', 'frost', 'frown', 'frozen', 'fruit', 'fuel', 'fun', 'funny', 'furnace', 'fury', 'future', 'gadget', 'gain', 'galaxy', 'gallery', 'game', 'gap', 'garage', 'garbage', 'garden', 'garlic', 'garment', 'gas', 'gasp', 'gate', 'gather', 'gauge', 'gaze', 'general', 'genius', 'genre', 'gentle', 'genuine', 'gesture', 'ghost', 'giant', 'gift', 'giggle', 'ginger', 'giraffe', 'girl', 'give', 'glad', 'glance', 'glare', 'glass', 'glide', 'glimpse', 'globe', 'gloom', 'glory', 'glove', 'glow', 'glue', 'goat', 'goddess', 'gold', 'good', 'goose', 'gorilla', 'gospel', 'gossip', 'govern', 'gown', 'grab', 'grace', 'grain', 'grant', 'grape', 'grass', 'gravity', 'great', 'green', 'grid', 'grief', 'grit', 'grocery', 'group', 'grow', 'grunt', 'guard', 'guess', 'guide', 'guilt', 'guitar', 'gun', 'gym', 'habit', 'hair', 'half', 'hammer', 'hamster', 'hand', 'happy', 'harbor', 'hard', 'harsh', 'harvest', 'hat', 'have', 'hawk', 'hazard', 'head', 'health', 'heart', 'heavy', 'hedgehog', 'height', 'hello', 'helmet', 'help', 'hen', 'hero', 'hidden', 'high', 'hill', 'hint', 'hip', 'hire', 'history', 'hobby', 'hockey', 'hold', 'hole', 'holiday', 'hollow', 'home', 'honey', 'hood', 'hope', 'horn', 'horror', 'horse', 'hospital', 'host', 'hotel', 'hour', 'hover', 'hub', 'huge', 'human', 'humble', 'humor', 'hundred', 'hungry', 'hunt', 'hurdle', 'hurry', 'hurt', 'husband', 'hybrid', 'ice', 'icon', 'idea', 'identify', 'idle', 'ignore', 'ill', 'illegal', 'illness', 'image', 'imitate', 'immense', 'immune', 'impact', 'impose', 'improve', 'impulse', 'inch', 'include', 'income', 'increase', 'index', 'indicate', 'indoor', 'industry', 'infant', 'inflict', 'inform', 'inhale', 'inherit', 'initial', 'inject', 'injury', 'inmate', 'inner', 'innocent', 'input', 'inquiry', 'insane', 'insect', 'inside', 'inspire', 'install', 'intact', 'interest', 'into', 'invest', 'invite', 'involve', 'iron', 'island', 'isolate', 'issue', 'item', 'ivory', 'jacket', 'jaguar', 'jar', 'jazz', 'jealous', 'jeans', 'jelly', 'jewel', 'job', 'join', 'joke', 'journey', 'joy', 'judge', 'juice', 'jump', 'jungle', 'junior', 'junk', 'just', 'kangaroo', 'keen', 'keep', 'ketchup', 'key', 'kick', 'kid', 'kidney', 'kind', 'kingdom', 'kiss', 'kit', 'kitchen', 'kite', 'kitten', 'kiwi', 'knee', 'knife', 'knock', 'know', 'lab', 'label', 'labor', 'ladder', 'lady', 'lake', 'lamp', 'language', 'laptop', 'large', 'later', 'latin', 'laugh', 'laundry', 'lava', 'law', 'lawn', 'lawsuit', 'layer', 'lazy', 'leader', 'leaf', 'learn', 'leave', 'lecture', 'left', 'leg', 'legal', 'legend', 'leisure', 'lemon', 'lend', 'length', 'lens', 'leopard', 'lesson', 'letter', 'level', 'liar', 'liberty', 'library', 'license', 'life', 'lift', 'light', 'like', 'limb', 'limit', 'link', 'lion', 'liquid', 'list', 'little', 'live', 'lizard', 'load', 'loan', 'lobster', 'local', 'lock', 'logic', 'lonely', 'long', 'loop', 'lottery', 'loud', 'lounge', 'love', 'loyal', 'lucky', 'luggage', 'lumber', 'lunar', 'lunch', 'luxury', 'lyrics', 'machine', 'mad', 'magic', 'magnet', 'maid', 'mail', 'main', 'major', 'make', 'mammal', 'man', 'manage', 'mandate', 'mango', 'mansion', 'manual', 'maple', 'marble', 'march', 'margin', 'marine', 'market', 'marriage', 'mask', 'mass', 'master', 'match', 'material', 'math', 'matrix', 'matter', 'maximum', 'maze', 'meadow', 'mean', 'measure', 'meat', 'mechanic', 'medal', 'media', 'melody', 'melt', 'member', 'memory', 'mention', 'menu', 'mercy', 'merge', 'merit', 'merry', 'mesh', 'message', 'metal', 'method', 'middle', 'midnight', 'milk', 'million', 'mimic', 'mind', 'minimum', 'minor', 'minute', 'miracle', 'mirror', 'misery', 'miss', 'mistake', 'mix', 'mixed', 'mixture', 'mobile', 'model', 'modify', 'mom', 'moment', 'monitor', 'monkey', 'monster', 'month', 'moon', 'moral', 'more', 'morning', 'mosquito', 'mother', 'motion', 'motor', 'mountain', 'mouse', 'move', 'movie', 'much', 'muffin', 'mule', 'multiply', 'muscle', 'museum', 'mushroom', 'music', 'must', 'mutual', 'myself', 'mystery', 'myth', 'naive', 'name', 'napkin', 'narrow', 'nasty', 'nation', 'nature', 'near', 'neck', 'need', 'negative', 'neglect', 'neither', 'nephew', 'nerve', 'nest', 'net', 'network', 'neutral', 'never', 'news', 'next', 'nice', 'night', 'noble', 'noise', 'nominee', 'noodle', 'normal', 'north', 'nose', 'notable', 'note', 'nothing', 'notice', 'novel', 'now', 'nuclear', 'number', 'nurse', 'nut', 'oak', 'obey', 'object', 'oblige', 'obscure', 'observe', 'obtain', 'obvious', 'occur', 'ocean', 'october', 'odor', 'off', 'offer', 'office', 'often', 'oil', 'okay', 'old', 'olive', 'olympic', 'omit', 'once', 'one', 'onion', 'online', 'only', 'open', 'opera', 'opinion', 'oppose', 'option', 'orange', 'orbit', 'orchard', 'order', 'ordinary', 'organ', 'orient', 'original', 'orphan', 'ostrich', 'other', 'outdoor', 'outer', 'output', 'outside', 'oval', 'oven', 'over', 'own', 'owner', 'oxygen', 'oyster', 'ozone', 'pact', 'paddle', 'page', 'pair', 'palace', 'palm', 'panda', 'panel', 'panic', 'panther', 'paper', 'parade', 'parent', 'park', 'parrot', 'party', 'pass', 'patch', 'path', 'patient', 'patrol', 'pattern', 'pause', 'pave', 'payment', 'peace', 'peanut', 'pear', 'peasant', 'pelican', 'pen', 'penalty', 'pencil', 'people', 'pepper', 'perfect', 'permit', 'person', 'pet', 'phone', 'photo', 'phrase', 'physical', 'piano', 'picnic', 'picture', 'piece', 'pig', 'pigeon', 'pill', 'pilot', 'pink', 'pioneer', 'pipe', 'pistol', 'pitch', 'pizza', 'place', 'planet', 'plastic', 'plate', 'play', 'please', 'pledge', 'pluck', 'plug', 'plunge', 'poem', 'poet', 'point', 'polar', 'pole', 'police', 'pond', 'pony', 'pool', 'popular', 'portion', 'position', 'possible', 'post', 'potato', 'pottery', 'poverty', 'powder', 'power', 'practice', 'praise', 'predict', 'prefer', 'prepare', 'present', 'pretty', 'prevent', 'price', 'pride', 'primary', 'print', 'priority', 'prison', 'private', 'prize', 'problem', 'process', 'produce', 'profit', 'program', 'project', 'promote', 'proof', 'property', 'prosper', 'protect', 'proud', 'provide', 'public', 'pudding', 'pull', 'pulp', 'pulse', 'pumpkin', 'punch', 'pupil', 'puppy', 'purchase', 'purity', 'purpose', 'purse', 'push', 'put', 'puzzle', 'pyramid', 'quality', 'quantum', 'quarter', 'question', 'quick', 'quit', 'quiz', 'quote', 'rabbit', 'raccoon', 'race', 'rack', 'radar', 'radio', 'rail', 'rain', 'raise', 'rally', 'ramp', 'ranch', 'random', 'range', 'rapid', 'rare', 'rate', 'rather', 'raven', 'raw', 'razor', 'ready', 'real', 'reason', 'rebel', 'rebuild', 'recall', 'receive', 'recipe', 'record', 'recycle', 'reduce', 'reflect', 'reform', 'refuse', 'region', 'regret', 'regular', 'reject', 'relax', 'release', 'relief', 'rely', 'remain', 'remember', 'remind', 'remove', 'render', 'renew', 'rent', 'reopen', 'repair', 'repeat', 'replace', 'report', 'require', 'rescue', 'resemble', 'resist', 'resource', 'response', 'result', 'retire', 'retreat', 'return', 'reunion', 'reveal', 'review', 'reward', 'rhythm', 'rib', 'ribbon', 'rice', 'rich', 'ride', 'ridge', 'rifle', 'right', 'rigid', 'ring', 'riot', 'ripple', 'risk', 'ritual', 'rival', 'river', 'road', 'roast', 'robot', 'robust', 'rocket', 'romance', 'roof', 'rookie', 'room', 'rose', 'rotate', 'rough', 'round', 'route', 'royal', 'rubber', 'rude', 'rug', 'rule', 'run', 'runway', 'rural', 'sad', 'saddle', 'sadness', 'safe', 'sail', 'salad', 'salmon', 'salon', 'salt', 'salute', 'same', 'sample', 'sand', 'satisfy', 'satoshi', 'sauce', 'sausage', 'save', 'say', 'scale', 'scan', 'scare', 'scatter', 'scene', 'scheme', 'school', 'science', 'scissors', 'scorpion', 'scout', 'scrap', 'screen', 'script', 'scrub', 'sea', 'search', 'season', 'seat', 'second', 'secret', 'section', 'security', 'seed', 'seek', 'segment', 'select', 'sell', 'seminar', 'senior', 'sense', 'sentence', 'series', 'service', 'session', 'settle', 'setup', 'seven', 'shadow', 'shaft', 'shallow', 'share', 'shed', 'shell', 'sheriff', 'shield', 'shift', 'shine', 'ship', 'shiver', 'shock', 'shoe', 'shoot', 'shop', 'short', 'shoulder', 'shove', 'shrimp', 'shrug', 'shuffle', 'shy', 'sibling', 'sick', 'side', 'siege', 'sight', 'sign', 'silent', 'silk', 'silly', 'silver', 'similar', 'simple', 'since', 'sing', 'siren', 'sister', 'situate', 'six', 'size', 'skate', 'sketch', 'ski', 'skill', 'skin', 'skirt', 'skull', 'slab', 'slam', 'sleep', 'slender', 'slice', 'slide', 'slight', 'slim', 'slogan', 'slot', 'slow', 'slush', 'small', 'smart', 'smile', 'smoke', 'smooth', 'snack', 'snake', 'snap', 'sniff', 'snow', 'soap', 'soccer', 'social', 'sock', 'soda', 'soft', 'solar', 'soldier', 'solid', 'solution', 'solve', 'someone', 'song', 'soon', 'sorry', 'sort', 'soul', 'sound', 'soup', 'source', 'south', 'space', 'spare', 'spatial', 'spawn', 'speak', 'special', 'speed', 'spell', 'spend', 'sphere', 'spice', 'spider', 'spike', 'spin', 'spirit', 'split', 'spoil', 'sponsor', 'spoon', 'sport', 'spot', 'spray', 'spread', 'spring', 'spy', 'square', 'squeeze', 'squirrel', 'stable', 'stadium', 'staff', 'stage', 'stairs', 'stamp', 'stand', 'start', 'state', 'stay', 'steak', 'steel', 'stem', 'step', 'stereo', 'stick', 'still', 'sting', 'stock', 'stomach', 'stone', 'stool', 'story', 'stove', 'strategy', 'street', 'strike', 'strong', 'struggle', 'student', 'stuff', 'stumble', 'style', 'subject', 'submit', 'subway', 'success', 'such', 'sudden', 'suffer', 'sugar', 'suggest', 'suit', 'summer', 'sun', 'sunny', 'sunset', 'super', 'supply', 'supreme', 'sure', 'surface', 'surge', 'surprise', 'surround', 'survey', 'suspect', 'sustain', 'swallow', 'swamp', 'swap', 'swarm', 'swear', 'sweet', 'swift', 'swim', 'swing', 'switch', 'sword', 'symbol', 'symptom', 'syrup', 'system', 'table', 'tackle', 'tag', 'tail', 'talent', 'talk', 'tank', 'tape', 'target', 'task', 'taste', 'tattoo', 'taxi', 'teach', 'team', 'tell', 'ten', 'tenant', 'tennis', 'tent', 'term', 'test', 'text', 'thank', 'that', 'theme', 'then', 'theory', 'there', 'they', 'thing', 'this', 'thought', 'three', 'thrive', 'throw', 'thumb', 'thunder', 'ticket', 'tide', 'tiger', 'tilt', 'timber', 'time', 'tiny', 'tip', 'tired', 'tissue', 'title', 'toast', 'tobacco', 'today', 'toddler', 'toe', 'together', 'toilet', 'token', 'tomato', 'tomorrow', 'tone', 'tongue', 'tonight', 'tool', 'tooth', 'top', 'topic', 'topple', 'torch', 'tornado', 'tortoise', 'toss', 'total', 'tourist', 'toward', 'tower', 'town', 'toy', 'track', 'trade', 'traffic', 'tragic', 'train', 'transfer', 'trap', 'trash', 'travel', 'tray', 'treat', 'tree', 'trend', 'trial', 'tribe', 'trick', 'trigger', 'trim', 'trip', 'trophy', 'trouble', 'truck', 'true', 'truly', 'trumpet', 'trust', 'truth', 'try', 'tube', 'tuition', 'tumble', 'tuna', 'tunnel', 'turkey', 'turn', 'turtle', 'twelve', 'twenty', 'twice', 'twin', 'twist', 'two', 'type', 'typical', 'ugly', 'umbrella', 'unable', 'unaware', 'uncle', 'uncover', 'under', 'undo', 'unfair', 'unfold', 'unhappy', 'uniform', 'unique', 'unit', 'universe', 'unknown', 'unlock', 'until', 'unusual', 'unveil', 'update', 'upgrade', 'uphold', 'upon', 'upper', 'upset', 'urban', 'urge', 'usage', 'use', 'used', 'useful', 'useless', 'usual', 'utility', 'vacant', 'vacuum', 'vague', 'valid', 'valley', 'valve', 'van', 'vanish', 'vapor', 'various', 'vast', 'vault', 'vehicle', 'velvet', 'vendor', 'venture', 'venue', 'verb', 'verify', 'version', 'very', 'vessel', 'veteran', 'viable', 'vibrant', 'vicious', 'victory', 'video', 'view', 'village', 'vintage', 'violin', 'virtual', 'virus', 'visa', 'visit', 'visual', 'vital', 'vivid', 'vocal', 'voice', 'void', 'volcano', 'volume', 'vote', 'voyage', 'wage', 'wagon', 'wait', 'walk', 'wall', 'walnut', 'want', 'warfare', 'warm', 'warrior', 'wash', 'wasp', 'waste', 'water', 'wave', 'way', 'wealth', 'weapon', 'wear', 'weasel', 'weather', 'web', 'wedding', 'weekend', 'weird', 'welcome', 'west', 'wet', 'whale', 'what', 'wheat', 'wheel', 'when', 'where', 'whip', 'whisper', 'wide', 'width', 'wife', 'wild', 'will', 'win', 'window', 'wine', 'wing', 'wink', 'winner', 'winter', 'wire', 'wisdom', 'wise', 'wish', 'witness', 'wolf', 'woman', 'wonder', 'wood', 'wool', 'word', 'work', 'world', 'worry', 'worth', 'wrap', 'wreck', 'wrestle', 'wrist', 'write', 'wrong', 'yard', 'year', 'yellow', 'you', 'young', 'youth', 'zebra', 'zero', 'zone', 'zoo'];
@@ -0,0 +1,28 @@
1
+ /**
2
+ * #1532 — a browser and the kind of device it runs on, in words a person
3
+ * recognises ("Chrome on Windows"), from a User-Agent header. Used where a
4
+ * person is asked to judge a request someone made: the sign-in approval
5
+ * message and page. The rules are the ones `suggestedName()` in
6
+ * `public/js/passkey.js` applies in the browser, so a passkey's suggested name
7
+ * and an approval request describe the same device the same way.
8
+ *
9
+ * Null when the header names neither a browser nor a device we know.
10
+ */
11
+ export function describeBrowser(userAgent) {
12
+ const ua = userAgent ?? '';
13
+ const browser = /Edg(e|A|iOS)?\//.test(ua) ? 'Edge'
14
+ : /Firefox\/|FxiOS\//.test(ua) ? 'Firefox'
15
+ : /Chrome\/|CriOS\//.test(ua) ? 'Chrome'
16
+ : /Safari\//.test(ua) ? 'Safari'
17
+ : null;
18
+ const device = /iPhone/.test(ua) ? 'iPhone'
19
+ : /iPad/.test(ua) ? 'iPad'
20
+ : /Android/.test(ua) ? 'Android phone'
21
+ : /Macintosh|Mac OS X/.test(ua) ? 'Mac'
22
+ : /Windows/.test(ua) ? 'Windows'
23
+ : /Linux/.test(ua) ? 'Linux'
24
+ : null;
25
+ if (browser && device)
26
+ return `${browser} on ${device}`;
27
+ return browser ?? device;
28
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * What a delegated credential may never carry (#946, #1198, #1576).
3
+ *
4
+ * A person delegates a subset of their own permissions to something acting
5
+ * for them: an agent token they mint, or an app they allow through the OpenID
6
+ * Connect provider. Two kinds of permission are never delegable, however
7
+ * privileged the person:
8
+ *
9
+ * - `admin-*` — administration stays with a person at the keyboard
10
+ * (#946 decision 3);
11
+ * - `token-mint` — a delegation never creates a standing credential, or a
12
+ * leaked one could breed (#1198);
13
+ * - `token-extend` — a delegation never lengthens a standing credential, or a
14
+ * leaked token could keep itself alive (#1734). Step-up already refuses it
15
+ * to a delegation, but a site may turn step-up off; this does not move;
16
+ * - `proxy-grant` — a token or an app never lets someone act for the person
17
+ * (#1762). A proxy grant itself may carry it (that is how a proxy passes
18
+ * access on), so this rule is for tokens and apps, not proxy grants;
19
+ * - `vault-proxy` — it means something only inside a proxy grant, where it
20
+ * lets the proxy into the person's vaults (#1762 slice 2); on a token or an
21
+ * app it would read as access it can never give.
22
+ *
23
+ * One rule for both delegations, so they cannot drift.
24
+ */
25
+ /** Actions a delegation may never carry, by prefix. */
26
+ export const FORBIDDEN_DELEGATED_PREFIX = 'admin-';
27
+ /** The permission to mint a token — never carried by a delegation. */
28
+ export const MINT_PERMISSION = 'token-mint';
29
+ /** The permission to extend a token — never carried by a delegation (#1734). */
30
+ export const EXTEND_PERMISSION = 'token-extend';
31
+ /** The permission to make a proxy grant — never carried by a token or an app (#1762). */
32
+ export const PROXY_GRANT_PERMISSION = 'proxy-grant';
33
+ /** The proxy grant's vault marker — never carried by a token or an app (#1762). */
34
+ export const VAULT_PROXY_PERMISSION = 'vault-proxy';
35
+ /** Why a scope cannot be delegated, or null when it can. */
36
+ export function refusedDelegatedScope(scope) {
37
+ if (scope.startsWith(FORBIDDEN_DELEGATED_PREFIX))
38
+ return `${scope} is an admin permission, which is never delegated`;
39
+ if (scope === MINT_PERMISSION)
40
+ return `${MINT_PERMISSION} is never delegated: a delegation never mints a token`;
41
+ if (scope === EXTEND_PERMISSION)
42
+ return `${EXTEND_PERMISSION} is never delegated: a delegation never extends a token`;
43
+ if (scope === VAULT_PROXY_PERMISSION)
44
+ return `${VAULT_PROXY_PERMISSION} is only for proxy grants: it lets someone acting for you into your vaults`;
45
+ if (scope === PROXY_GRANT_PERMISSION)
46
+ return `${PROXY_GRANT_PERMISSION} is never delegated: a token or an app never lets someone act for you`;
47
+ return null;
48
+ }
49
+ /**
50
+ * A delegation's permission list as given (#946, #1762): at least one, each a
51
+ * non-empty string, de-duplicated in order. Throws a caller-safe message. One
52
+ * rule for agent tokens and proxy grants.
53
+ */
54
+ export function requireScopeList(scopes) {
55
+ // An unscoped delegation is rejected, never treated as unrestricted (#946 decision 4).
56
+ if (!Array.isArray(scopes) || scopes.length === 0) {
57
+ throw new Error('At least one scope is required');
58
+ }
59
+ // Scopes arrive from a request body. A non-string here would be stored and
60
+ // then match no action for the life of the delegation — a credential that
61
+ // fails silently rather than at the point of the mistake.
62
+ if (!scopes.every((s) => typeof s === 'string' && s.trim().length > 0)) {
63
+ throw new Error('Every scope must be a non-empty string');
64
+ }
65
+ return [...new Set(scopes)];
66
+ }
67
+ /**
68
+ * "Delegate, not grant" (#1178, #1762): the scopes the delegator does not hold,
69
+ * in order. Empty means every scope is theirs to hand on. `holds` is asked of
70
+ * the policy decision point; with no answer there is no delegation.
71
+ */
72
+ export async function lackingScopes(scopes, holds) {
73
+ const lacking = [];
74
+ for (const scope of scopes) {
75
+ if (!(await holds(scope)))
76
+ lacking.push(scope);
77
+ }
78
+ return lacking;
79
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Derive a username from an email address (#1026).
3
+ *
4
+ * Extracted from GoogleOIDCProvider so every auto-provisioning path — OIDC and
5
+ * magic link — produces the same username for the same address. Two rules that
6
+ * disagree would let one provider create `jim-smith` and the other `jimsmith`
7
+ * for the same person.
8
+ */
9
+ /** True if the username is already taken. Treats a lookup error as "free". */
10
+ async function usernameExists(username, userManager) {
11
+ try {
12
+ const u = await userManager.getUser(username);
13
+ return u !== undefined;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ /**
20
+ * Turn an email address into an unused username.
21
+ *
22
+ * Local part, lowercased, with anything outside `[a-z0-9]` replaced by a
23
+ * hyphen; a numeric suffix is appended until the name is free.
24
+ *
25
+ * The result is only free as of this call — a caller that derives a name and
26
+ * creates the account later must be prepared for `createUser` to reject a
27
+ * duplicate that appeared in between.
28
+ */
29
+ export async function deriveUsername(email, userManager) {
30
+ const base = email.split('@')[0].toLowerCase().replace(/[^a-z0-9]/g, '-');
31
+ let candidate = base;
32
+ let n = 2;
33
+ while (await usernameExists(candidate, userManager)) {
34
+ candidate = `${base}-${n++}`;
35
+ }
36
+ return candidate;
37
+ }
38
+ export default deriveUsername;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * emailForLog — the one way an email address reaches the server log (#1916).
3
+ *
4
+ * Rule (operator, 2026-10-20): a full email address never reaches the server
5
+ * log. A log line names the address's domain only. Audit records are not the
6
+ * server log and keep full addresses; do not use these helpers on them.
7
+ */
8
+ /**
9
+ * The domain(s) of one address or a comma-separated list, lower-cased:
10
+ * `"Bob <bob@Example.org>, ann@x.io"` → `"example.org,x.io"`. A value
11
+ * without an `@` (or an empty one) reads as `unknown`.
12
+ */
13
+ export function emailDomain(to) {
14
+ const domains = new Set(String(to ?? '').split(',').map((a) => a.trim()).filter(Boolean)
15
+ .map((a) => (a.includes('@') ? a.slice(a.lastIndexOf('@') + 1).replace(/>$/, '').toLowerCase() : 'unknown')));
16
+ return [...domains].join(',') || 'unknown';
17
+ }
18
+ /**
19
+ * Any address inside free text (an SMTP error often quotes the rejected one)
20
+ * cut to its domain: `bob@example.org` → `…@example.org`.
21
+ */
22
+ export function redactEmailAddresses(text) {
23
+ return text.replace(/[^\s<>"'@,;:()[\]]+@([A-Za-z0-9.-]+)/g, '…@$1');
24
+ }
25
+ /** An error's message, with any address in it cut to its domain. */
26
+ export function errorForLog(err) {
27
+ return redactEmailAddresses(err instanceof Error ? err.message : String(err));
28
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The files migration dry run (#1859, epic #1851): what copying today's
3
+ * attachments into files WOULD do, worked out from the door's inventory
4
+ * (`FileManager.inventory`). Pure: it reads nothing and writes nothing.
5
+ *
6
+ * The rules are the decision log's
7
+ * (https://github.com/jwilleke/ngdpbase/issues/1851#issuecomment-6236003946):
8
+ * - two different files with the same name in one place get a numeric
9
+ * suffix, `report.pdf` and `report-001.pdf`;
10
+ * - each page that shows a renamed file is rewritten to its new name, so it
11
+ * keeps showing its own file;
12
+ * - an unknown or invalid uploader gets an owner from configuration.
13
+ */
14
+ import type { FileInventory, FileRecord, VaultFileSummary } from '../types/File.js';
15
+ /** The configured values the plan applies (`ngdpbase.files.migration.*`). */
16
+ export interface FileMigrationOptions {
17
+ /** Recorded uploader → username, applied first (`owner-map`). */
18
+ ownerMap: Record<string, string>;
19
+ /** Owner for a file whose record names no user (`default-owner`). */
20
+ defaultOwner: string;
21
+ /** Width of the clash suffix: 3 gives `-001` (`clash-suffix-digits`). */
22
+ suffixDigits: number;
23
+ }
24
+ /** A file the migration would give a new name, and the pages it would rewrite to it. */
25
+ export interface PlannedRename {
26
+ id: string;
27
+ from: string;
28
+ to: string;
29
+ pages: string[];
30
+ }
31
+ /** Who a group of public files would belong to after the migration. */
32
+ export interface PlannedOwner {
33
+ /** The uploader the record names, or `null` when it names none. */
34
+ recorded: string | null;
35
+ owner: string;
36
+ /** `kept`: the record names a user; `mapped`: `owner-map`; `default`: `default-owner`. */
37
+ how: 'kept' | 'mapped' | 'default';
38
+ files: number;
39
+ }
40
+ export interface FileMigrationReport {
41
+ publicFiles: {
42
+ count: number;
43
+ bytes: number;
44
+ byType: Array<{
45
+ type: string;
46
+ files: number;
47
+ }>;
48
+ };
49
+ owners: PlannedOwner[];
50
+ /** Public files whose record names no user (before the owner mapping). */
51
+ noOwner: number;
52
+ /** Each clashing name, with the renames it would get. */
53
+ clashes: Array<{
54
+ name: string;
55
+ files: number;
56
+ renames: PlannedRename[];
57
+ }>;
58
+ /** Each page that would be rewritten, and to which names. */
59
+ pagesToRewrite: Array<{
60
+ page: string;
61
+ changes: Array<{
62
+ from: string;
63
+ to: string;
64
+ }>;
65
+ }>;
66
+ vaults: VaultFileSummary[];
67
+ }
68
+ /** The `-###` name a clash gets: before the extension, `route.png` → `route-001.png`. */
69
+ export declare function suffixedName(name: string, n: number, digits: number): string;
70
+ /**
71
+ * The new name of every file in one place that shares its name with another
72
+ * (id → new name). The oldest keeps the name (then the lowest id, so the
73
+ * answer is the same on every run); the rest are numbered from 1 in that
74
+ * order, skipping any name already used in the place. Names match exactly,
75
+ * as a link finds a file today.
76
+ */
77
+ export declare function planClashRenames(files: ReadonlyArray<Pick<FileRecord, 'id' | 'filename' | 'dateCreated'>>, digits: number): Map<string, string>;
78
+ /** What the migration would do with this inventory. */
79
+ export declare function planFileMigration(inventory: FileInventory, opts: FileMigrationOptions): FileMigrationReport;
80
+ /** The report as the operator reads it. */
81
+ export declare function formatFileMigrationReport(r: FileMigrationReport, header?: string[]): string;
@@ -9,6 +9,12 @@ export declare function instanceEnvPath(instanceDataDir: string): string;
9
9
  * (`dotenv.parse`, `bootstrap-env.ts`): never a value (#1976).
10
10
  */
11
11
  export declare function envKeyNames(content: string): string[];
12
+ /**
13
+ * One `.env` line setting `name` to `value` (#2134): unquoted, or quoted only
14
+ * when it must be, so the boot's reader (`dotenv.parse`) reads `value` back
15
+ * exactly. Throws for a value no single line can carry.
16
+ */
17
+ export declare function envFileLine(name: string, value: string): string;
12
18
  /** A short fingerprint of a set of `.env` names (#1976): the first 16 hex digits of a SHA-256 of the sorted names, one per line. */
13
19
  export declare function envKeyNamesFingerprint(names: readonly string[]): string;
14
20
  /** The file without any line assigning one of `names`. */
@@ -46,6 +46,19 @@ export function instanceEnvPath(instanceDataDir) {
46
46
  export function envKeyNames(content) {
47
47
  return Object.keys(dotenv.parse(content)).sort();
48
48
  }
49
+ /**
50
+ * One `.env` line setting `name` to `value` (#2134): unquoted, or quoted only
51
+ * when it must be, so the boot's reader (`dotenv.parse`) reads `value` back
52
+ * exactly. Throws for a value no single line can carry.
53
+ */
54
+ export function envFileLine(name, value) {
55
+ for (const form of [value, `'${value}'`, `"${value}"`, `\`${value}\``]) {
56
+ const line = `${name}=${form}`;
57
+ if (dotenv.parse(line)[name] === value)
58
+ return line;
59
+ }
60
+ throw new Error(`${name} cannot be written as one .env line`);
61
+ }
49
62
  /** A short fingerprint of a set of `.env` names (#1976): the first 16 hex digits of a SHA-256 of the sorted names, one per line. */
50
63
  export function envKeyNamesFingerprint(names) {
51
64
  return createHash('sha256').update([...names].sort().join('\n')).digest('hex').slice(0, 16);
@@ -0,0 +1,106 @@
1
+ /**
2
+ * #1532 — roughly where a request came from, in words a person recognises
3
+ * ("Columbus, United States"), for the sign-in approval message and page.
4
+ *
5
+ * Two sources, both optional, both off unless the operator turns them on;
6
+ * nothing is bundled and nothing is looked up outside the site:
7
+ *
8
+ * - Cloudflare's visitor-location headers (`cf-ipcity`, `cf-ipcountry`), only
9
+ * when the site says it is reached through Cloudflare. Anyone can send these
10
+ * headers to a site that is not behind Cloudflare, so they are never read
11
+ * unless the operator says so.
12
+ * - A MaxMind GeoLite2 City database (`.mmdb`) the operator supplies, read
13
+ * with `mmdb-lib` (no dependencies of its own). Loaded on first use and
14
+ * again when the file changes; one that cannot be read is said once in the
15
+ * log and gives no location.
16
+ *
17
+ * Null when neither says anything: the location is left out, never guessed.
18
+ */
19
+ import { readFile, stat } from 'node:fs/promises';
20
+ import { isIPv4, isIPv6 } from 'node:net';
21
+ import { Reader } from 'mmdb-lib';
22
+ import logger from './logger.js';
23
+ /** A place name from a header or a database: printable, one line, short. */
24
+ function placeName(value) {
25
+ if (typeof value !== 'string')
26
+ return null;
27
+ // eslint-disable-next-line no-control-regex
28
+ const tidy = value.replace(/[\u0000-\u001f\u007f]/g, '').replace(/\s+/g, ' ').trim().slice(0, 80).trim();
29
+ return tidy || null;
30
+ }
31
+ /** "United States" for "US"; null for anything that is not a real two-letter country (XX unknown, T1 Tor). */
32
+ function countryName(code) {
33
+ if (typeof code !== 'string' || !/^[A-Z]{2}$/.test(code) || code === 'XX' || code === 'T1')
34
+ return null;
35
+ try {
36
+ const name = new Intl.DisplayNames(['en'], { type: 'region' }).of(code);
37
+ return name && name !== code ? name : code;
38
+ }
39
+ catch {
40
+ return code;
41
+ }
42
+ }
43
+ function joined(city, country) {
44
+ const parts = [city, country].filter((p) => Boolean(p));
45
+ return parts.length ? parts.join(', ') : null;
46
+ }
47
+ /** An IPv4 address carried as IPv6 (::ffff:1.2.3.4) is looked up as the IPv4 address. */
48
+ function plainAddress(ip) {
49
+ if (!ip)
50
+ return null;
51
+ const v4 = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i.exec(ip)?.[1];
52
+ const address = v4 ?? ip;
53
+ return isIPv4(address) || isIPv6(address) ? address : null;
54
+ }
55
+ let loaded = null;
56
+ const warned = new Set();
57
+ async function cityReader(file) {
58
+ try {
59
+ const { mtimeMs } = await stat(file);
60
+ if (loaded?.file === file && loaded.mtimeMs === mtimeMs)
61
+ return loaded.reader;
62
+ const reader = new Reader(await readFile(file));
63
+ loaded = { file, mtimeMs, reader };
64
+ warned.delete(file);
65
+ return reader;
66
+ }
67
+ catch (err) {
68
+ if (!warned.has(file)) {
69
+ warned.add(file);
70
+ logger.warn(`[ipLocation] the GeoLite2 City file ${file} could not be read; sign-in approvals say no location (#1532): ${err.message}`);
71
+ }
72
+ return null;
73
+ }
74
+ }
75
+ /** What the GeoLite2 City file says about `ip`, or null. */
76
+ async function fromGeolite(file, ip) {
77
+ const address = plainAddress(ip);
78
+ if (!address)
79
+ return null;
80
+ const reader = await cityReader(file);
81
+ if (!reader)
82
+ return null;
83
+ // An IPv4-only database has no IPv6 addresses; mmdb-lib would walk it anyway.
84
+ if (reader.metadata.ipVersion === 4 && !isIPv4(address))
85
+ return null;
86
+ let found;
87
+ try {
88
+ found = reader.get(address);
89
+ }
90
+ catch {
91
+ return null;
92
+ }
93
+ return joined(placeName(found?.city?.names?.en), countryName(found?.country?.iso_code) ?? placeName(found?.country?.names?.en));
94
+ }
95
+ /**
96
+ * Roughly where `ip` is: Cloudflare's headers when the site is behind it and
97
+ * they name a place, else the GeoLite2 file when one is configured, else null.
98
+ */
99
+ export async function approximateLocation(ip, header, sources) {
100
+ if (sources.behindCloudflare) {
101
+ const fromCloudflare = joined(placeName(header('cf-ipcity')), countryName(header('cf-ipcountry')));
102
+ if (fromCloudflare)
103
+ return fromCloudflare;
104
+ }
105
+ return sources.geoliteFile ? fromGeolite(sources.geoliteFile, ip) : null;
106
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Magic-link device binding (#1022).
3
+ *
4
+ * A magic link is bearer-only: whoever holds the URL can sign in. A forwarded
5
+ * email, a link pasted into a chat, or a mailbox a second person can read is a
6
+ * complete account takeover — no second factor, and nothing in the log saying
7
+ * the redemption came from somewhere unexpected.
8
+ *
9
+ * The mitigation is to remember which browser asked for the link (an opaque
10
+ * value in an HTTP-only cookie, stored alongside the token) and check it at
11
+ * redemption.
12
+ *
13
+ * ## Why this is two features, not one
14
+ *
15
+ * Strict binding breaks the most common real flow: request the link on a
16
+ * laptop, open the mail on a phone. That is not an edge case, and silently
17
+ * breaking it would be worse than the risk being mitigated. So:
18
+ *
19
+ * - **Observability is always on.** Every redemption records whether the
20
+ * browser matched, plus IP and User-Agent, through `AuditManager` so it is
21
+ * queryable rather than buried in the app log. Costs nothing in UX.
22
+ * - **Enforcement is opt-in**, behind
23
+ * `ngdpbase.auth.magic-link.bind-to-requesting-device` (default `false`).
24
+ *
25
+ * ## The direction that must not be got wrong
26
+ *
27
+ * An absent cookie is `absent`, never `match`. A forwarded link arrives with no
28
+ * cookie at all, so treating "no cookie" as a pass would make the enforcement
29
+ * setting do nothing against the exact attacker it exists to stop.
30
+ *
31
+ * A token carrying no stored state is a separate case (`unknown`) and is always
32
+ * allowed: tokens live in memory for ~15 minutes, so on the deploy that ships
33
+ * this there are live tokens issued before the feature existed. Refusing those
34
+ * would sign people out mid-flow for no security gain.
35
+ */
36
+ import * as crypto from 'crypto';
37
+ /** Cookie carrying the opaque per-request state value. */
38
+ export const DEVICE_STATE_COOKIE = 'ngdp_ml_state';
39
+ /**
40
+ * The same binding for an OAuth sign-in (#1630): the browser that started the
41
+ * flow is the one the callback must come back to. Its own cookie, so a
42
+ * magic-link request and a Google sign-in in flight together do not overwrite
43
+ * each other. Unlike the magic link there is no cross-device flow here — the
44
+ * provider redirects the same browser back — so it is always enforced.
45
+ */
46
+ export const OAUTH_STATE_COOKIE = 'ngdp_oauth_state';
47
+ /** A fresh opaque state value. 32 bytes of CSPRNG, hex-encoded. */
48
+ export function newDeviceState() {
49
+ return crypto.randomBytes(32).toString('hex');
50
+ }
51
+ /**
52
+ * Cookie options for the state cookie.
53
+ *
54
+ * `SameSite=Lax` rather than `Strict` is deliberate: the redemption arrives as
55
+ * a top-level navigation from a mail client or webmail tab, and `Strict` would
56
+ * withhold the cookie on that cross-site click — making every ordinary
57
+ * redemption look like a mismatch, which is the failure mode most likely to get
58
+ * this feature switched off.
59
+ *
60
+ * `httpOnly` keeps page scripts from reading or forging it. `maxAge` tracks the
61
+ * token TTL so the cookie does not outlive what it describes.
62
+ */
63
+ export function deviceStateCookieOptions(ttlMs, secure = false) {
64
+ return { httpOnly: true, sameSite: 'lax', maxAge: ttlMs, path: '/', secure };
65
+ }
66
+ /**
67
+ * Compare two same-length secrets without leaking position through timing.
68
+ *
69
+ * Different lengths short-circuit to false — the length is already observable
70
+ * from the cookie itself, so there is nothing to protect there.
71
+ */
72
+ function safeEqual(a, b) {
73
+ const bufA = Buffer.from(a, 'utf8');
74
+ const bufB = Buffer.from(b, 'utf8');
75
+ if (bufA.length !== bufB.length)
76
+ return false;
77
+ return crypto.timingSafeEqual(bufA, bufB);
78
+ }
79
+ /**
80
+ * Decide what the presented cookie says about the redeeming browser.
81
+ *
82
+ * Never throws: this runs on the sign-in path, and an exception here would turn
83
+ * a legitimate login into a 500.
84
+ */
85
+ export function evaluateDeviceBinding(input) {
86
+ const { stored, presented, enforce } = input;
87
+ // No stored state: the token predates the feature. Not evidence of anything,
88
+ // so it cannot be grounds for refusal.
89
+ if (!stored)
90
+ return { outcome: 'unknown', allowed: true };
91
+ if (!presented)
92
+ return { outcome: 'absent', allowed: !enforce };
93
+ const matched = safeEqual(stored, presented);
94
+ return matched
95
+ ? { outcome: 'match', allowed: true }
96
+ : { outcome: 'mismatch', allowed: !enforce };
97
+ }
@@ -14,5 +14,6 @@ export type PageAction = 'view' | 'edit' | 'save' | 'delete' | 'history' | 'diff
14
14
  *
15
15
  * @example pageUrl('Main') === '/view/Main'
16
16
  * @example pageUrl('vaults/jim/default/Diary', 'edit') === '/vaults/jim/default/Diary/edit'
17
+ * @example pageUrl('shared/default/Minutes') === '/shared/default/Minutes' (#2125)
17
18
  */
18
19
  export declare function pageUrl(pageName: string, action?: PageAction): string;