@bunizao/contracts 0.7.2 → 0.9.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.
@@ -60,6 +60,9 @@ export interface ReaderMe {
60
60
  provider: ReaderProvider;
61
61
  displayName: string;
62
62
  avatarUrl: string;
63
+ /** Seed of the drawn face shown when `avatarUrl` is empty. Null until one
64
+ has been handed out -- the client then draws from the name. */
65
+ avatarSeed: number | null;
63
66
  notifyReplies: boolean;
64
67
  /** Whether this address holds an active newsletter subscription. */
65
68
  subscribed: boolean;
@@ -101,9 +104,10 @@ export interface ReaderMuteResult {
101
104
  so this can never be used to probe which addresses have commented. */
102
105
  export interface ReaderResendInput {
103
106
  email: string;
104
- /** Carries the original "notify me of replies" intent into the fresh
105
- verification link, recovered from the stale token's payload. Optional;
106
- a bare resend defaults to false. */
107
+ /** Newsletter opt-in, despite the name: carried into the fresh
108
+ verification link from the stale token's payload, and confirming the
109
+ address activates a newsletter subscription. Not the reply-mail switch
110
+ (that is `ReaderMe.notifyReplies`). Optional; a bare resend sends false. */
107
111
  notifyReplies?: boolean;
108
112
  locale?: CommentLocale;
109
113
  }
@@ -131,6 +135,8 @@ export declare const DEFAULT_REACTION_EMOJI = "\u2764\uFE0F";
131
135
  export interface ReactorChip {
132
136
  name: string;
133
137
  avatarUrl: string | null;
138
+ /** Drawn-face seed; absent from servers older than 0.8.0. */
139
+ avatarSeed?: number | null;
134
140
  }
135
141
  export interface ReactionSummary {
136
142
  emoji: string;
@@ -304,6 +310,10 @@ export interface CommentAuthor {
304
310
  any key it does not know, so a path emitted for every address would make
305
311
  every face an identicon. */
306
312
  avatarUrl: string;
313
+ /** Seed of the drawn face for an empty `avatarUrl`: the writer's reader
314
+ seed when they have one, else the seed the comment was posted with.
315
+ Null or absent (servers older than 0.8.0) means draw from the name. */
316
+ avatarSeed?: number | null;
307
317
  /** True when this row's writer is the blog owner. */
308
318
  byAuthor: boolean;
309
319
  }
@@ -361,14 +371,19 @@ export interface CommentCreateInput {
361
371
  Omitted or empty: the comment is owned by its anon session only and
362
372
  the client renders an identicon. */
363
373
  email?: string;
374
+ /** The drawn face the writer picked in the compose box, from
375
+ `READER_AVATAR_SEED_PATH`. Ignored when it is not an `isAvatarSeed`. */
376
+ avatarSeed?: number;
364
377
  turnstileToken: string;
365
378
  /** Visually-hidden honeypot field. Must arrive empty. */
366
379
  website?: string;
367
380
  /** Signed server timestamp minted at first interaction with the compose
368
381
  box — see "The risk stack" step 4 (dwell time). */
369
382
  dwellToken: string;
370
- /** Reader accepted the post-comment subscribe offer. Only takes effect
371
- once the address is verified. */
383
+ /** Newsletter opt-in, despite the name: the reader accepted the
384
+ post-comment subscribe offer, and confirming the address activates a
385
+ newsletter subscription. Not the reply-mail switch (that is
386
+ `ReaderMe.notifyReplies`). Both clients send false. */
372
387
  notifyReplies?: boolean;
373
388
  locale?: CommentLocale;
374
389
  /** Client evidence, optional and never a gate -- see `ClientEvidence`. */
@@ -426,47 +441,10 @@ export interface CommentPolicy {
426
441
  requireVerifiedEmail: boolean;
427
442
  }
428
443
  export declare const DEFAULT_COMMENT_POLICY: CommentPolicy;
429
- /** Ghost internal tags that override the site-wide default, one knob each.
430
- *
431
- * Internal tags are the switch because they are the only per-post field the
432
- * author already edits in Ghost, and both halves of the system can read them:
433
- * the site sees them through the Admin API at build time, site-api through the
434
- * Content API at request time (`include=tags` returns internal tags). One
435
- * source of truth, no settings table, no admin screen to keep in sync.
436
- *
437
- * `#no-comments` predates the others and is kept: it always meant "this post
438
- * takes no more comments", which is `readonly`. */
439
444
  export declare const COMMENT_POLICY_TAGS: {
440
- readonly 'comments-off': (policy: CommentPolicy) => {
441
- /** Whether the heart can be pressed — on the post and on its comments.
442
- Independent of `mode`: a post can take reactions with comments off, and
443
- an open thread can refuse them. */
444
- reactions: boolean;
445
- /** Accept a comment only from an address that has been verified. Anonymous
446
- and unverified-email writers are refused rather than held. */
447
- requireVerifiedEmail: boolean;
448
- mode: CommentsMode;
449
- };
450
- readonly 'comments-readonly': (policy: CommentPolicy) => {
451
- /** Whether the heart can be pressed — on the post and on its comments.
452
- Independent of `mode`: a post can take reactions with comments off, and
453
- an open thread can refuse them. */
454
- reactions: boolean;
455
- /** Accept a comment only from an address that has been verified. Anonymous
456
- and unverified-email writers are refused rather than held. */
457
- requireVerifiedEmail: boolean;
458
- mode: CommentsMode;
459
- };
460
- readonly 'no-comments': (policy: CommentPolicy) => {
461
- /** Whether the heart can be pressed — on the post and on its comments.
462
- Independent of `mode`: a post can take reactions with comments off, and
463
- an open thread can refuse them. */
464
- reactions: boolean;
465
- /** Accept a comment only from an address that has been verified. Anonymous
466
- and unverified-email writers are refused rather than held. */
467
- requireVerifiedEmail: boolean;
468
- mode: CommentsMode;
469
- };
445
+ readonly 'comments-off': (policy: CommentPolicy) => CommentPolicy;
446
+ readonly 'comments-readonly': (policy: CommentPolicy) => CommentPolicy;
447
+ readonly 'no-comments': (policy: CommentPolicy) => CommentPolicy;
470
448
  readonly 'reactions-off': (policy: CommentPolicy) => {
471
449
  mode: CommentsMode;
472
450
  /** Accept a comment only from an address that has been verified. Anonymous
@@ -490,7 +468,8 @@ export interface CommentPolicyTagLike {
490
468
  slug?: string | null;
491
469
  }
492
470
  /** Fold a post's tags onto the site-wide default. Order does not matter:
493
- every tag sets one field, and a tag the map does not know is ignored. */
471
+ every tag sets one field, conflicting mode tags keep the stricter mode, and
472
+ a tag the map does not know is ignored. */
494
473
  export declare function commentPolicyFromTags(tags: readonly CommentPolicyTagLike[] | null | undefined, base?: CommentPolicy): CommentPolicy;
495
474
  /** Whether the post takes new comments — the one question the API asks. Both
496
475
  `readonly` and `off` answer no; the difference between them is drawn, not
@@ -523,3 +502,29 @@ export interface CommentTelemetryInput {
523
502
  outcome: 'accepted' | 'http_error' | 'network_error' | 'challenge_failed';
524
503
  challenges: number;
525
504
  }
505
+ /** A drawn face's colour pair is `seed % AVATAR_CLASSES`: five palette
506
+ colours as background, times the four others as head. The pair is what
507
+ tells two faces apart at stack size, so it is the unit the server balances
508
+ when it hands out a seed and the unit a re-roll must change. The palette
509
+ itself is a rendering concern and lives with the client. */
510
+ export declare const AVATAR_CLASSES = 20;
511
+ export declare const MAX_AVATAR_SEED = 4294967295;
512
+ export declare function isAvatarSeed(value: unknown): value is number;
513
+ export declare function avatarClass(seed: number): number;
514
+ /** A seed in colour class `cls`; `variety` fills the digits that pick the
515
+ expression, pose and head shape. */
516
+ export declare function seedInClass(cls: number, variety: number): number;
517
+ /** POST body for `READER_AVATAR_SEED_PATH`. */
518
+ export interface AvatarSeedInput {
519
+ /** The seed on screen now. The answer is always in a different colour
520
+ class, so every re-roll visibly changes the face. */
521
+ current?: number | null;
522
+ }
523
+ export interface AvatarSeedResult {
524
+ /** In the least-used colour class across the site, never `current`'s. */
525
+ seed: number;
526
+ /** True when the caller is a verified reader and the seed is now theirs
527
+ everywhere. False for anyone else: the client keeps it and sends it as
528
+ `CommentCreateInput.avatarSeed`. */
529
+ persisted: boolean;
530
+ }
package/dist/comments.js CHANGED
@@ -32,10 +32,11 @@ var DEFAULT_COMMENT_POLICY = {
32
32
  reactions: true,
33
33
  requireVerifiedEmail: false
34
34
  };
35
+ var withStricterMode = (policy, mode) => COMMENTS_MODES.indexOf(mode) > COMMENTS_MODES.indexOf(policy.mode) ? { ...policy, mode } : policy;
35
36
  var COMMENT_POLICY_TAGS = {
36
- "comments-off": (policy) => ({ ...policy, mode: "off" }),
37
- "comments-readonly": (policy) => ({ ...policy, mode: "readonly" }),
38
- "no-comments": (policy) => ({ ...policy, mode: "readonly" }),
37
+ "comments-off": (policy) => withStricterMode(policy, "off"),
38
+ "comments-readonly": (policy) => withStricterMode(policy, "readonly"),
39
+ "no-comments": (policy) => withStricterMode(policy, "readonly"),
39
40
  "reactions-off": (policy) => ({ ...policy, reactions: false }),
40
41
  "comments-verified": (policy) => ({ ...policy, requireVerifiedEmail: true })
41
42
  };
@@ -61,7 +62,20 @@ function commentPolicyFromTags(tags, base = DEFAULT_COMMENT_POLICY) {
61
62
  return policy;
62
63
  }
63
64
  var acceptsComments = (policy) => policy.mode === "open";
65
+ var AVATAR_CLASSES = 20;
66
+ var MAX_AVATAR_SEED = 4294967295;
67
+ function isAvatarSeed(value) {
68
+ return Number.isInteger(value) && value >= 0 && value <= MAX_AVATAR_SEED;
69
+ }
70
+ function avatarClass(seed) {
71
+ return seed % AVATAR_CLASSES;
72
+ }
73
+ function seedInClass(cls, variety) {
74
+ const span = Math.floor(MAX_AVATAR_SEED / AVATAR_CLASSES);
75
+ return Math.abs(Math.trunc(variety)) % span * AVATAR_CLASSES + Math.abs(Math.trunc(cls)) % AVATAR_CLASSES;
76
+ }
64
77
  export {
78
+ AVATAR_CLASSES,
65
79
  COMMENTS_MODES,
66
80
  COMMENT_ANCHOR_PATTERN,
67
81
  COMMENT_ANCHOR_TOKEN_LENGTH,
@@ -72,6 +86,7 @@ export {
72
86
  COMMENT_SURFACES,
73
87
  DEFAULT_COMMENT_POLICY,
74
88
  DEFAULT_REACTION_EMOJI,
89
+ MAX_AVATAR_SEED,
75
90
  MODERATION_ACTIONS,
76
91
  MODERATION_REASONS,
77
92
  REACTION_TARGET_TYPES,
@@ -80,6 +95,9 @@ export {
80
95
  READER_PROVIDERS,
81
96
  READER_VERIFY_OUTCOMES,
82
97
  acceptsComments,
98
+ avatarClass,
83
99
  commentAnchorToken,
84
- commentPolicyFromTags
100
+ commentPolicyFromTags,
101
+ isAvatarSeed,
102
+ seedInClass
85
103
  };
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export * from './analytics';
2
2
  export * from './admin';
3
3
  export * from './comments';
4
4
  export * from './content';
5
+ export * from './instagram';
5
6
  export * from './listening';
6
7
  export * from './messages';
7
8
  export * from './mood';
package/dist/index.js CHANGED
@@ -44,10 +44,11 @@ var DEFAULT_COMMENT_POLICY = {
44
44
  reactions: true,
45
45
  requireVerifiedEmail: false
46
46
  };
47
+ var withStricterMode = (policy, mode) => COMMENTS_MODES.indexOf(mode) > COMMENTS_MODES.indexOf(policy.mode) ? { ...policy, mode } : policy;
47
48
  var COMMENT_POLICY_TAGS = {
48
- "comments-off": (policy) => ({ ...policy, mode: "off" }),
49
- "comments-readonly": (policy) => ({ ...policy, mode: "readonly" }),
50
- "no-comments": (policy) => ({ ...policy, mode: "readonly" }),
49
+ "comments-off": (policy) => withStricterMode(policy, "off"),
50
+ "comments-readonly": (policy) => withStricterMode(policy, "readonly"),
51
+ "no-comments": (policy) => withStricterMode(policy, "readonly"),
51
52
  "reactions-off": (policy) => ({ ...policy, reactions: false }),
52
53
  "comments-verified": (policy) => ({ ...policy, requireVerifiedEmail: true })
53
54
  };
@@ -73,6 +74,18 @@ function commentPolicyFromTags(tags, base = DEFAULT_COMMENT_POLICY) {
73
74
  return policy;
74
75
  }
75
76
  var acceptsComments = (policy) => policy.mode === "open";
77
+ var AVATAR_CLASSES = 20;
78
+ var MAX_AVATAR_SEED = 4294967295;
79
+ function isAvatarSeed(value) {
80
+ return Number.isInteger(value) && value >= 0 && value <= MAX_AVATAR_SEED;
81
+ }
82
+ function avatarClass(seed) {
83
+ return seed % AVATAR_CLASSES;
84
+ }
85
+ function seedInClass(cls, variety) {
86
+ const span = Math.floor(MAX_AVATAR_SEED / AVATAR_CLASSES);
87
+ return Math.abs(Math.trunc(variety)) % span * AVATAR_CLASSES + Math.abs(Math.trunc(cls)) % AVATAR_CLASSES;
88
+ }
76
89
  // src/content.ts
77
90
  var CONTENT_DOCUMENT_SOURCES = ["mood", "post"];
78
91
  var POST_LOCALE_RE = /^[a-z]{2,8}(?:-[a-z0-9]{1,8})*$/;
@@ -119,6 +132,8 @@ var MOOD_SEARCH_PATH = "/v2/mood/search";
119
132
  var MOOD_IMAGE_PROXY_BASE_PATH = "/v2/images";
120
133
  var MOOD_MEDIA_PROXY_BASE_PATH = "/v2/media";
121
134
  var LISTENING_PATH = "/v2/listening";
135
+ var INSTAGRAM_PROFILE_PATH = "/v2/instagram";
136
+ var INSTAGRAM_AVATAR_PATH = "/v2/instagram/avatar";
122
137
  var COMMENTS_PATH = "/v2/comments";
123
138
  var COMMENT_PATH_PREFIX = "/v2/comments/";
124
139
  var OWNER_MESSAGES_PATH = "/v2/messages";
@@ -130,6 +145,7 @@ var READER_RESEND_PATH = "/v2/reader/resend";
130
145
  var READER_PREFERENCES_PATH = "/v2/reader/preferences";
131
146
  var READER_MUTE_PATH = "/v2/reader/mute";
132
147
  var READER_AVATAR_PATH_PREFIX = "/v2/reader/avatar/";
148
+ var READER_AVATAR_SEED_PATH = "/v2/reader/avatar-seed";
133
149
  var READER_OAUTH_PATH_PREFIX = "/oauth/reader/";
134
150
  var READER_CONFIRM_PATH = "/reader/confirm";
135
151
  var READER_MUTE_PAGE_PATH = "/reader/mute";
@@ -203,6 +219,7 @@ var TELEGRAM_OPS_BROADCAST_STATUSES = [
203
219
  export {
204
220
  ADMIN_BASE_PATH,
205
221
  API_PREFIX,
222
+ AVATAR_CLASSES,
206
223
  BLOG_ANALYTICS_ARTICLE_ENDPOINT,
207
224
  BLOG_ANALYTICS_COMPLETION_SCROLL_DEPTH,
208
225
  BLOG_ANALYTICS_EVENTS_DEFAULT_LIMIT,
@@ -227,6 +244,8 @@ export {
227
244
  EVENT_STATUSES,
228
245
  GHOST_WEBHOOK_PATH,
229
246
  HEALTH_PATH,
247
+ INSTAGRAM_AVATAR_PATH,
248
+ INSTAGRAM_PROFILE_PATH,
230
249
  LEGACY_ADMIN_BASE_PATH,
231
250
  LEGACY_GHOST_WEBHOOK_PATH,
232
251
  LEGACY_HEALTH_PATH,
@@ -234,6 +253,7 @@ export {
234
253
  LEGACY_NOTIFY_BASE_PATH,
235
254
  LISTENING_ANALYTICS_EVENT_ENDPOINT,
236
255
  LISTENING_PATH,
256
+ MAX_AVATAR_SEED,
237
257
  MESSAGE_LOCALES,
238
258
  MESSAGE_MAX_BODY_LENGTH,
239
259
  MESSAGE_MAX_NAME_LENGTH,
@@ -269,6 +289,7 @@ export {
269
289
  REACTIONS_TOGGLE_PATH,
270
290
  REACTION_TARGET_TYPES,
271
291
  READER_AVATAR_PATH_PREFIX,
292
+ READER_AVATAR_SEED_PATH,
272
293
  READER_CONFIRM_PATH,
273
294
  READER_GRADES,
274
295
  READER_ME_PATH,
@@ -298,9 +319,12 @@ export {
298
319
  TELEGRAM_OPS_WEBHOOK_PATH,
299
320
  TELEGRAM_WEBHOOK_PATH,
300
321
  acceptsComments,
322
+ avatarClass,
301
323
  commentAnchorToken,
302
324
  commentPolicyFromTags,
325
+ isAvatarSeed,
303
326
  parsePostLocaleTag,
327
+ seedInClass,
304
328
  telegramOpsEventActionPath,
305
329
  telegramOpsEventPath,
306
330
  telegramOpsReminderDeliveredPath,
@@ -0,0 +1,29 @@
1
+ export interface InstagramProfileCounts {
2
+ posts: number;
3
+ followers: number;
4
+ following: number;
5
+ }
6
+ export interface InstagramProfileAvatar {
7
+ /** Absolute URL of the stored picture, served by site-api. */
8
+ url: string;
9
+ contentType: string;
10
+ bytes: number;
11
+ /** Hex SHA-256 of the stored bytes; the avatar route sends it as the ETag. */
12
+ sha256: string;
13
+ }
14
+ export interface InstagramProfileAttempt {
15
+ at: string;
16
+ ok: boolean;
17
+ /** Why the attempt failed, e.g. `profile:401`; null when it succeeded. */
18
+ error: string | null;
19
+ }
20
+ export interface InstagramProfile {
21
+ username: string;
22
+ fullName: string;
23
+ profileUrl: string;
24
+ avatar: InstagramProfileAvatar;
25
+ counts: InstagramProfileCounts;
26
+ /** When the stored profile was last read from Instagram. */
27
+ refreshedAt: string;
28
+ lastAttempt: InstagramProfileAttempt;
29
+ }
File without changes
package/dist/routes.d.ts CHANGED
@@ -12,6 +12,8 @@ export declare const MOOD_SEARCH_PATH: '/v2/mood/search';
12
12
  export declare const MOOD_IMAGE_PROXY_BASE_PATH: '/v2/images';
13
13
  export declare const MOOD_MEDIA_PROXY_BASE_PATH: '/v2/media';
14
14
  export declare const LISTENING_PATH: '/v2/listening';
15
+ export declare const INSTAGRAM_PROFILE_PATH: '/v2/instagram';
16
+ export declare const INSTAGRAM_AVATAR_PATH: '/v2/instagram/avatar';
15
17
  export declare const COMMENTS_PATH: '/v2/comments';
16
18
  export declare const COMMENT_PATH_PREFIX: '/v2/comments/';
17
19
  export declare const OWNER_MESSAGES_PATH: '/v2/messages';
@@ -23,6 +25,7 @@ export declare const READER_RESEND_PATH: '/v2/reader/resend';
23
25
  export declare const READER_PREFERENCES_PATH: '/v2/reader/preferences';
24
26
  export declare const READER_MUTE_PATH: '/v2/reader/mute';
25
27
  export declare const READER_AVATAR_PATH_PREFIX: '/v2/reader/avatar/';
28
+ export declare const READER_AVATAR_SEED_PATH: '/v2/reader/avatar-seed';
26
29
  export declare const READER_OAUTH_PATH_PREFIX: '/oauth/reader/';
27
30
  export declare const READER_CONFIRM_PATH: '/reader/confirm';
28
31
  export declare const READER_MUTE_PAGE_PATH: '/reader/mute';
package/dist/routes.js CHANGED
@@ -13,6 +13,8 @@ var MOOD_SEARCH_PATH = "/v2/mood/search";
13
13
  var MOOD_IMAGE_PROXY_BASE_PATH = "/v2/images";
14
14
  var MOOD_MEDIA_PROXY_BASE_PATH = "/v2/media";
15
15
  var LISTENING_PATH = "/v2/listening";
16
+ var INSTAGRAM_PROFILE_PATH = "/v2/instagram";
17
+ var INSTAGRAM_AVATAR_PATH = "/v2/instagram/avatar";
16
18
  var COMMENTS_PATH = "/v2/comments";
17
19
  var COMMENT_PATH_PREFIX = "/v2/comments/";
18
20
  var OWNER_MESSAGES_PATH = "/v2/messages";
@@ -24,6 +26,7 @@ var READER_RESEND_PATH = "/v2/reader/resend";
24
26
  var READER_PREFERENCES_PATH = "/v2/reader/preferences";
25
27
  var READER_MUTE_PATH = "/v2/reader/mute";
26
28
  var READER_AVATAR_PATH_PREFIX = "/v2/reader/avatar/";
29
+ var READER_AVATAR_SEED_PATH = "/v2/reader/avatar-seed";
27
30
  var READER_OAUTH_PATH_PREFIX = "/oauth/reader/";
28
31
  var READER_CONFIRM_PATH = "/reader/confirm";
29
32
  var READER_MUTE_PAGE_PATH = "/reader/mute";
@@ -44,6 +47,8 @@ export {
44
47
  COMMENT_PATH_PREFIX,
45
48
  GHOST_WEBHOOK_PATH,
46
49
  HEALTH_PATH,
50
+ INSTAGRAM_AVATAR_PATH,
51
+ INSTAGRAM_PROFILE_PATH,
47
52
  LEGACY_ADMIN_BASE_PATH,
48
53
  LEGACY_GHOST_WEBHOOK_PATH,
49
54
  LEGACY_HEALTH_PATH,
@@ -67,6 +72,7 @@ export {
67
72
  REACTIONS_PATH,
68
73
  REACTIONS_TOGGLE_PATH,
69
74
  READER_AVATAR_PATH_PREFIX,
75
+ READER_AVATAR_SEED_PATH,
70
76
  READER_CONFIRM_PATH,
71
77
  READER_ME_PATH,
72
78
  READER_MUTE_PAGE_PATH,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunizao/contracts",
3
- "version": "0.7.2",
3
+ "version": "0.9.0",
4
4
  "description": "Shared TypeScript contracts for buxx.me services and clients.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -48,6 +48,11 @@
48
48
  "import": "./dist/content.js",
49
49
  "default": "./dist/content.js"
50
50
  },
51
+ "./instagram": {
52
+ "types": "./dist/instagram.d.ts",
53
+ "import": "./dist/instagram.js",
54
+ "default": "./dist/instagram.js"
55
+ },
51
56
  "./listening": {
52
57
  "types": "./dist/listening.d.ts",
53
58
  "import": "./dist/listening.js",