@atproto/bsky 0.1.7 → 0.2.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 (129) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/dist/api/app/bsky/actor/getProfile.js +1 -1
  3. package/dist/api/app/bsky/actor/getProfile.js.map +1 -1
  4. package/dist/api/app/bsky/actor/getProfiles.js +1 -1
  5. package/dist/api/app/bsky/actor/getProfiles.js.map +1 -1
  6. package/dist/api/app/bsky/actor/getSuggestions.js +2 -2
  7. package/dist/api/app/bsky/actor/getSuggestions.js.map +1 -1
  8. package/dist/api/app/bsky/embed/getEmbedExternalView.d.ts.map +1 -1
  9. package/dist/api/app/bsky/embed/getEmbedExternalView.js +132 -53
  10. package/dist/api/app/bsky/embed/getEmbedExternalView.js.map +1 -1
  11. package/dist/api/app/bsky/feed/getAuthorFeed.d.ts.map +1 -1
  12. package/dist/api/app/bsky/feed/getAuthorFeed.js +4 -2
  13. package/dist/api/app/bsky/feed/getAuthorFeed.js.map +1 -1
  14. package/dist/api/app/bsky/graph/getSuggestedFollowsByActor.js +2 -2
  15. package/dist/api/app/bsky/graph/getSuggestedFollowsByActor.js.map +1 -1
  16. package/dist/api/app/bsky/notification/constants.d.ts +4 -1
  17. package/dist/api/app/bsky/notification/constants.d.ts.map +1 -1
  18. package/dist/api/app/bsky/notification/constants.js +3 -0
  19. package/dist/api/app/bsky/notification/constants.js.map +1 -1
  20. package/dist/api/app/bsky/notification/getGroupedNotifications.js +19 -7
  21. package/dist/api/app/bsky/notification/getGroupedNotifications.js.map +1 -1
  22. package/dist/api/app/bsky/notification/grouping/spotlight.d.ts +1 -1
  23. package/dist/api/app/bsky/notification/grouping/spotlight.d.ts.map +1 -1
  24. package/dist/api/app/bsky/notification/grouping/spotlight.js +114 -80
  25. package/dist/api/app/bsky/notification/grouping/spotlight.js.map +1 -1
  26. package/dist/api/app/bsky/notification/listActivitySubscriptions.js +3 -2
  27. package/dist/api/app/bsky/notification/listActivitySubscriptions.js.map +1 -1
  28. package/dist/api/app/bsky/unspecced/getAtmosphereExploreTab.d.ts.map +1 -1
  29. package/dist/api/app/bsky/unspecced/getAtmosphereExploreTab.js +16 -148
  30. package/dist/api/app/bsky/unspecced/getAtmosphereExploreTab.js.map +1 -1
  31. package/dist/api/internal/bsky/actor/getProfiles.js +4 -1
  32. package/dist/api/internal/bsky/actor/getProfiles.js.map +1 -1
  33. package/dist/config.d.ts +2 -0
  34. package/dist/config.d.ts.map +1 -1
  35. package/dist/config.js +7 -0
  36. package/dist/config.js.map +1 -1
  37. package/dist/data-plane/server/indexing/index.d.ts +3 -1
  38. package/dist/data-plane/server/indexing/index.d.ts.map +1 -1
  39. package/dist/data-plane/server/indexing/index.js +2 -0
  40. package/dist/data-plane/server/indexing/index.js.map +1 -1
  41. package/dist/data-plane/server/indexing/plugins/profile-link.d.ts +8 -0
  42. package/dist/data-plane/server/indexing/plugins/profile-link.d.ts.map +1 -0
  43. package/dist/data-plane/server/indexing/plugins/profile-link.js +30 -0
  44. package/dist/data-plane/server/indexing/plugins/profile-link.js.map +1 -0
  45. package/dist/data-plane/server/routes/records.d.ts.map +1 -1
  46. package/dist/data-plane/server/routes/records.js +1 -0
  47. package/dist/data-plane/server/routes/records.js.map +1 -1
  48. package/dist/hydration/actor.d.ts +4 -1
  49. package/dist/hydration/actor.d.ts.map +1 -1
  50. package/dist/hydration/actor.js +11 -0
  51. package/dist/hydration/actor.js.map +1 -1
  52. package/dist/hydration/external.d.ts +67 -64
  53. package/dist/hydration/external.d.ts.map +1 -1
  54. package/dist/hydration/external.js +79 -133
  55. package/dist/hydration/external.js.map +1 -1
  56. package/dist/hydration/hydrator.d.ts +53 -14
  57. package/dist/hydration/hydrator.d.ts.map +1 -1
  58. package/dist/hydration/hydrator.js +414 -105
  59. package/dist/hydration/hydrator.js.map +1 -1
  60. package/dist/lexicons/app/bsky/actor/defs.defs.d.ts +24 -0
  61. package/dist/lexicons/app/bsky/actor/defs.defs.d.ts.map +1 -1
  62. package/dist/lexicons/app/bsky/actor/defs.defs.js +13 -0
  63. package/dist/lexicons/app/bsky/actor/defs.defs.js.map +1 -1
  64. package/dist/lexicons/app/bsky/actor/link.d.ts +3 -0
  65. package/dist/lexicons/app/bsky/actor/link.d.ts.map +1 -0
  66. package/dist/lexicons/app/bsky/actor/link.defs.d.ts +105 -0
  67. package/dist/lexicons/app/bsky/actor/link.defs.d.ts.map +1 -0
  68. package/dist/lexicons/app/bsky/actor/link.defs.js +34 -0
  69. package/dist/lexicons/app/bsky/actor/link.defs.js.map +1 -0
  70. package/dist/lexicons/app/bsky/actor/link.js +6 -0
  71. package/dist/lexicons/app/bsky/actor/link.js.map +1 -0
  72. package/dist/lexicons/app/bsky/actor/profile.defs.d.ts +14 -0
  73. package/dist/lexicons/app/bsky/actor/profile.defs.d.ts.map +1 -1
  74. package/dist/lexicons/app/bsky/actor/profile.defs.js +3 -0
  75. package/dist/lexicons/app/bsky/actor/profile.defs.js.map +1 -1
  76. package/dist/lexicons/app/bsky/actor.d.ts +1 -0
  77. package/dist/lexicons/app/bsky/actor.d.ts.map +1 -1
  78. package/dist/lexicons/app/bsky/actor.js +1 -0
  79. package/dist/lexicons/app/bsky/actor.js.map +1 -1
  80. package/dist/lexicons/app/bsky/authFullApp.defs.d.ts.map +1 -1
  81. package/dist/lexicons/app/bsky/authFullApp.defs.js +1 -0
  82. package/dist/lexicons/app/bsky/authFullApp.defs.js.map +1 -1
  83. package/dist/lexicons/app/bsky/authManageProfile.defs.d.ts.map +1 -1
  84. package/dist/lexicons/app/bsky/authManageProfile.defs.js +1 -0
  85. package/dist/lexicons/app/bsky/authManageProfile.defs.js.map +1 -1
  86. package/dist/lexicons/app/bsky/embed/getEmbedExternalView.defs.d.ts +13 -3
  87. package/dist/lexicons/app/bsky/embed/getEmbedExternalView.defs.d.ts.map +1 -1
  88. package/dist/lexicons/app/bsky/embed/getEmbedExternalView.defs.js +11 -3
  89. package/dist/lexicons/app/bsky/embed/getEmbedExternalView.defs.js.map +1 -1
  90. package/dist/lexicons/app/bsky/notification/getGroupedNotifications.defs.d.ts +62 -3
  91. package/dist/lexicons/app/bsky/notification/getGroupedNotifications.defs.d.ts.map +1 -1
  92. package/dist/lexicons/app/bsky/notification/getGroupedNotifications.defs.js +53 -2
  93. package/dist/lexicons/app/bsky/notification/getGroupedNotifications.defs.js.map +1 -1
  94. package/dist/proto/bsky_connect.d.ts +10 -1
  95. package/dist/proto/bsky_connect.d.ts.map +1 -1
  96. package/dist/proto/bsky_connect.js +10 -1
  97. package/dist/proto/bsky_connect.js.map +1 -1
  98. package/dist/proto/bsky_pb.d.ts +34 -0
  99. package/dist/proto/bsky_pb.d.ts.map +1 -1
  100. package/dist/proto/bsky_pb.js +60 -0
  101. package/dist/proto/bsky_pb.js.map +1 -1
  102. package/dist/telemetry/events.d.ts +30 -0
  103. package/dist/telemetry/events.d.ts.map +1 -1
  104. package/dist/telemetry/events.js +37 -0
  105. package/dist/telemetry/events.js.map +1 -1
  106. package/dist/util/external/index.d.ts +8 -0
  107. package/dist/util/external/index.d.ts.map +1 -0
  108. package/dist/util/external/index.js +50 -0
  109. package/dist/util/external/index.js.map +1 -0
  110. package/dist/util/{standard-site.d.ts → external/standard-site.d.ts} +30 -4
  111. package/dist/util/external/standard-site.d.ts.map +1 -0
  112. package/dist/util/{standard-site.js → external/standard-site.js} +73 -4
  113. package/dist/util/external/standard-site.js.map +1 -0
  114. package/dist/views/external.js +1 -1
  115. package/dist/views/external.js.map +1 -1
  116. package/dist/views/index.d.ts +10 -6
  117. package/dist/views/index.d.ts.map +1 -1
  118. package/dist/views/index.js +84 -42
  119. package/dist/views/index.js.map +1 -1
  120. package/dist/views/profile-link-rules.d.ts +10 -0
  121. package/dist/views/profile-link-rules.d.ts.map +1 -0
  122. package/dist/views/profile-link-rules.js +48 -0
  123. package/dist/views/profile-link-rules.js.map +1 -0
  124. package/dist/views/types.d.ts +2 -0
  125. package/dist/views/types.d.ts.map +1 -1
  126. package/dist/views/types.js.map +1 -1
  127. package/package.json +4 -4
  128. package/dist/util/standard-site.d.ts.map +0 -1
  129. package/dist/util/standard-site.js.map +0 -1
@@ -1,4 +1,24 @@
1
1
  export type HydrationSource = 'known_likers' | 'known_followers' | 'activity_subscriptions';
2
+ /** Entry point that owns an external hydration traversal. */
3
+ export type ExternalHydrationRoot = 'uris' | 'refs' | 'dependencies';
4
+ export type ExternalHydrationOutcome = 'completed' | 'capped' | 'failed';
5
+ /**
6
+ * Generic record lookup work of one external hydration traversal, across the
7
+ * root and all nested dependency batches. Excludes profile/label hydration,
8
+ * backlink sample/count RPCs, and legacy Standard Site reads.
9
+ */
10
+ export type ExternalHydrationWork = {
11
+ /**
12
+ * Accepted latest/exact lookup identities scheduled for fetching, after
13
+ * filtering duplicates and seen keys. Unavailable results and lookups in a
14
+ * failed batch count; lookups skipped at the pass cap do not.
15
+ */
16
+ recordLookups: number;
17
+ /** Nonempty batches that scheduled generic fetches; not a raw RPC count. */
18
+ batches: number;
19
+ /** Deepest fetched batch, where the root batch is pass 1; 0 if none. */
20
+ maxPass: number;
21
+ };
2
22
  /**
3
23
  * Central hub for reporting AppView events. Each method reports a
4
24
  * single event to every relevant sink in one call:
@@ -25,6 +45,16 @@ declare class EventReporter {
25
45
  source: HydrationSource;
26
46
  err: unknown;
27
47
  }): void;
48
+ /**
49
+ * Reports the totals of one top-level external hydration traversal. Numeric
50
+ * totals are observations and log fields, never metric attributes.
51
+ *
52
+ * @note this method should never throw
53
+ */
54
+ externalHydrationTraversal({ root, outcome, recordLookups, batches, maxPass, }: ExternalHydrationWork & {
55
+ root: ExternalHydrationRoot;
56
+ outcome: ExternalHydrationOutcome;
57
+ }): void;
28
58
  }
29
59
  export declare const events: EventReporter;
30
60
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/telemetry/events.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,eAAe,GACzB,cAAc,GAAG,iBAAiB,GAAG,wBAAwB,CAAA;AAE/D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,cAAM,aAAa;;IAoCjB,eAAe,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE;QAAE,MAAM,EAAE,eAAe,CAAC;QAAC,GAAG,EAAE,OAAO,CAAA;KAAE,QAQzE;CACF;AAED,eAAO,MAAM,MAAM,EAAE,aAAmC,CAAA"}
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/telemetry/events.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,eAAe,GACzB,cAAc,GAAG,iBAAiB,GAAG,wBAAwB,CAAA;AAE/D,6DAA6D;AAC7D,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,MAAM,GAAG,cAAc,CAAA;AACpE,MAAM,MAAM,wBAAwB,GAAG,WAAW,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAExE;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC;;;;OAIG;IACH,aAAa,EAAE,MAAM,CAAA;IACrB,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAA;IACf,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,cAAM,aAAa;;IA6DjB,eAAe,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE;QAAE,MAAM,EAAE,eAAe,CAAC;QAAC,GAAG,EAAE,OAAO,CAAA;KAAE,QAQzE;IAED;;;;;OAKG;IACH,0BAA0B,CAAC,EACzB,IAAI,EACJ,OAAO,EACP,aAAa,EACb,OAAO,EACP,OAAO,GACR,EAAE,qBAAqB,GAAG;QACzB,IAAI,EAAE,qBAAqB,CAAA;QAC3B,OAAO,EAAE,wBAAwB,CAAA;KAClC,QAcA;CACF;AAED,eAAO,MAAM,MAAM,EAAE,aAAmC,CAAA"}
@@ -29,6 +29,21 @@ class EventReporter {
29
29
  description: 'Number of fail-open hydration steps that did not produce a result',
30
30
  valueType: ValueType.INT,
31
31
  });
32
+ #externalHydrationLookups = meter.createHistogram('hydration.external.record_lookups', {
33
+ description: 'Generic external record lookups scheduled per hydration traversal, including nested dependency batches',
34
+ unit: '{lookup}',
35
+ valueType: ValueType.INT,
36
+ advice: {
37
+ explicitBucketBoundaries: [0, 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000],
38
+ },
39
+ });
40
+ #externalHydrationBatches = meter.createHistogram('hydration.external.batches', {
41
+ description: 'Generic external record fetch batches per hydration traversal, one per nested pass',
42
+ unit: '{batch}',
43
+ valueType: ValueType.INT,
44
+ // @NOTE Traversals are capped at `MAX_EXTERNAL_HYDRATION_PASSES` (8).
45
+ advice: { explicitBucketBoundaries: [0, 1, 2, 3, 4, 5, 6, 7, 8] },
46
+ });
32
47
  /**
33
48
  * Fans a single event out to both the pino logger (stdout/stderr) and the
34
49
  * OTEL Logs SDK (structured record).
@@ -60,6 +75,28 @@ class EventReporter {
60
75
  this.#log('hydration_failed', { source, reason });
61
76
  this.#hydrationFailedCounter.add(1, { source, reason });
62
77
  }
78
+ /**
79
+ * Reports the totals of one top-level external hydration traversal. Numeric
80
+ * totals are observations and log fields, never metric attributes.
81
+ *
82
+ * @note this method should never throw
83
+ */
84
+ externalHydrationTraversal({ root, outcome, recordLookups, batches, maxPass, }) {
85
+ this.#log('external_hydration_traversal', {
86
+ root,
87
+ outcome,
88
+ recordLookups,
89
+ batches,
90
+ maxPass,
91
+ });
92
+ try {
93
+ this.#externalHydrationLookups.record(recordLookups, { root, outcome });
94
+ this.#externalHydrationBatches.record(batches, { root, outcome });
95
+ }
96
+ catch (err) {
97
+ diag.error('Failed to record external_hydration_traversal:', err);
98
+ }
99
+ }
63
100
  }
64
101
  export const events = new EventReporter();
65
102
  //# sourceMappingURL=events.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/telemetry/events.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AACzE,OAAO,EAAe,cAAc,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAA;AAC3E,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAExD,MAAM,KAAK,GAAU,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAA;AACtD,MAAM,MAAM,GAAW,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC,CAAA;AAKtD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,aAAa;IACjB,uBAAuB,GAAG,KAAK,CAAC,aAAa,CAG1C,kBAAkB,EAAE;QACrB,WAAW,EACT,mEAAmE;QACrE,SAAS,EAAE,SAAS,CAAC,GAAG;KACzB,CAAC,CAAA;IAEF;;;;;OAKG;IACH,IAAI,CACF,SAAiB,EACjB,UAAiE;QAEjE,IAAI,CAAC;YACH,uDAAuD;YACvD,MAAM,CAAC,IAAI,CAAC;gBACV,SAAS;gBACT,cAAc,EAAE,cAAc,CAAC,IAAI;gBACnC,UAAU;aACX,CAAC,CAAA;YAEF,mDAAmD;YACnD,wDAAwD;YACxD,YAAY,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,GAAG,UAAU,EAAE,CAAC,CAAA;QACjD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,KAAK,CAAC,uBAAuB,SAAS,GAAG,EAAE,GAAG,CAAC,CAAA;QACtD,CAAC;IACH,CAAC;IAED,eAAe,CAAC,EAAE,MAAM,EAAE,GAAG,EAA6C;QACxE,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC;YAC9B,CAAC,CAAC,OAAO;YACT,CAAC,CAAC,cAAc,CAAC,GAAG,CAAC;gBACnB,CAAC,CAAC,SAAS;gBACX,CAAC,CAAC,OAAO,CAAA;QACb,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;QACjD,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;IACzD,CAAC;CACF;AAED,MAAM,CAAC,MAAM,MAAM,GAAkB,IAAI,aAAa,EAAE,CAAA","sourcesContent":["import { type Meter, ValueType, diag, metrics } from '@opentelemetry/api'\nimport { type Logger, SeverityNumber, logs } from '@opentelemetry/api-logs'\nimport { eventsLogger } from '../logger.js'\nimport { isAbortError, isTimeoutError } from './util.js'\n\nconst meter: Meter = metrics.getMeter('@atproto/bsky')\nconst logger: Logger = logs.getLogger('@atproto/bsky')\n\nexport type HydrationSource =\n 'known_likers' | 'known_followers' | 'activity_subscriptions'\n\n/**\n * Central hub for reporting AppView events. Each method reports a\n * single event to every relevant sink in one call:\n *\n * 1. a pino logger → stdout/stderr (docker logs)\n * 2. an OTEL counter → aggregated metric (low-cardinality dimensions only)\n * 3. the OTEL Logs SDK → structured, trace-correlated log record (full detail)\n *\n * This keeps the three concerns in sync: there is exactly one call site per\n * event, and the counter / log / attribute definitions live together here\n * rather than being duplicated and drifting across handlers.\n *\n * @note High-cardinality attributes (e.g. `did`, `uri`) belong in the pino and\n * OTEL log records but must stay OUT of the counters, whose attributes must\n * remain low-cardinality to avoid a metric-series explosion.\n *\n * @note The OTEL counters and log records are no-ops unless the corresponding\n * OTEL exporter is configured, so calling these methods is always safe and\n * essentially free when telemetry is disabled.\n */\nclass EventReporter {\n #hydrationFailedCounter = meter.createCounter<{\n source: HydrationSource\n reason: 'abort' | 'timeout' | 'error'\n }>('hydration.failed', {\n description:\n 'Number of fail-open hydration steps that did not produce a result',\n valueType: ValueType.INT,\n })\n\n /**\n * Fans a single event out to both the pino logger (stdout/stderr) and the\n * OTEL Logs SDK (structured record).\n *\n * @note this method should never throw\n */\n #log(\n eventName: string,\n attributes: Record<string, string | number | boolean | undefined>,\n ) {\n try {\n // Emits an OTEL log for the event (goes to collector).\n logger.emit({\n eventName,\n severityNumber: SeverityNumber.INFO,\n attributes,\n })\n\n // Emits a Pino log for the event (goes to stdout).\n // NOTE: We'll probably migrate towards only using OTEL.\n eventsLogger.info({ eventName, ...attributes })\n } catch (err) {\n diag.error(`Failed to log event ${eventName}:`, err)\n }\n }\n\n hydrationFailed({ source, err }: { source: HydrationSource; err: unknown }) {\n const reason = isAbortError(err)\n ? 'abort'\n : isTimeoutError(err)\n ? 'timeout'\n : 'error'\n this.#log('hydration_failed', { source, reason })\n this.#hydrationFailedCounter.add(1, { source, reason })\n }\n}\n\nexport const events: EventReporter = new EventReporter()\n"]}
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/telemetry/events.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Meter, ValueType, diag, metrics } from '@opentelemetry/api'\nimport { type Logger, SeverityNumber, logs } from '@opentelemetry/api-logs'\nimport { eventsLogger } from '../logger.js'\nimport { isAbortError, isTimeoutError } from './util.js'\n\nconst meter: Meter = metrics.getMeter('@atproto/bsky')\nconst logger: Logger = logs.getLogger('@atproto/bsky')\n\nexport type HydrationSource =\n 'known_likers' | 'known_followers' | 'activity_subscriptions'\n\n/** Entry point that owns an external hydration traversal. */\nexport type ExternalHydrationRoot = 'uris' | 'refs' | 'dependencies'\nexport type ExternalHydrationOutcome = 'completed' | 'capped' | 'failed'\n\n/**\n * Generic record lookup work of one external hydration traversal, across the\n * root and all nested dependency batches. Excludes profile/label hydration,\n * backlink sample/count RPCs, and legacy Standard Site reads.\n */\nexport type ExternalHydrationWork = {\n /**\n * Accepted latest/exact lookup identities scheduled for fetching, after\n * filtering duplicates and seen keys. Unavailable results and lookups in a\n * failed batch count; lookups skipped at the pass cap do not.\n */\n recordLookups: number\n /** Nonempty batches that scheduled generic fetches; not a raw RPC count. */\n batches: number\n /** Deepest fetched batch, where the root batch is pass 1; 0 if none. */\n maxPass: number\n}\n\n/**\n * Central hub for reporting AppView events. Each method reports a\n * single event to every relevant sink in one call:\n *\n * 1. a pino logger → stdout/stderr (docker logs)\n * 2. an OTEL counter → aggregated metric (low-cardinality dimensions only)\n * 3. the OTEL Logs SDK → structured, trace-correlated log record (full detail)\n *\n * This keeps the three concerns in sync: there is exactly one call site per\n * event, and the counter / log / attribute definitions live together here\n * rather than being duplicated and drifting across handlers.\n *\n * @note High-cardinality attributes (e.g. `did`, `uri`) belong in the pino and\n * OTEL log records but must stay OUT of the counters, whose attributes must\n * remain low-cardinality to avoid a metric-series explosion.\n *\n * @note The OTEL counters and log records are no-ops unless the corresponding\n * OTEL exporter is configured, so calling these methods is always safe and\n * essentially free when telemetry is disabled.\n */\nclass EventReporter {\n #hydrationFailedCounter = meter.createCounter<{\n source: HydrationSource\n reason: 'abort' | 'timeout' | 'error'\n }>('hydration.failed', {\n description:\n 'Number of fail-open hydration steps that did not produce a result',\n valueType: ValueType.INT,\n })\n\n #externalHydrationLookups = meter.createHistogram<{\n root: ExternalHydrationRoot\n outcome: ExternalHydrationOutcome\n }>('hydration.external.record_lookups', {\n description:\n 'Generic external record lookups scheduled per hydration traversal, including nested dependency batches',\n unit: '{lookup}',\n valueType: ValueType.INT,\n advice: {\n explicitBucketBoundaries: [0, 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000],\n },\n })\n\n #externalHydrationBatches = meter.createHistogram<{\n root: ExternalHydrationRoot\n outcome: ExternalHydrationOutcome\n }>('hydration.external.batches', {\n description:\n 'Generic external record fetch batches per hydration traversal, one per nested pass',\n unit: '{batch}',\n valueType: ValueType.INT,\n // @NOTE Traversals are capped at `MAX_EXTERNAL_HYDRATION_PASSES` (8).\n advice: { explicitBucketBoundaries: [0, 1, 2, 3, 4, 5, 6, 7, 8] },\n })\n\n /**\n * Fans a single event out to both the pino logger (stdout/stderr) and the\n * OTEL Logs SDK (structured record).\n *\n * @note this method should never throw\n */\n #log(\n eventName: string,\n attributes: Record<string, string | number | boolean | undefined>,\n ) {\n try {\n // Emits an OTEL log for the event (goes to collector).\n logger.emit({\n eventName,\n severityNumber: SeverityNumber.INFO,\n attributes,\n })\n\n // Emits a Pino log for the event (goes to stdout).\n // NOTE: We'll probably migrate towards only using OTEL.\n eventsLogger.info({ eventName, ...attributes })\n } catch (err) {\n diag.error(`Failed to log event ${eventName}:`, err)\n }\n }\n\n hydrationFailed({ source, err }: { source: HydrationSource; err: unknown }) {\n const reason = isAbortError(err)\n ? 'abort'\n : isTimeoutError(err)\n ? 'timeout'\n : 'error'\n this.#log('hydration_failed', { source, reason })\n this.#hydrationFailedCounter.add(1, { source, reason })\n }\n\n /**\n * Reports the totals of one top-level external hydration traversal. Numeric\n * totals are observations and log fields, never metric attributes.\n *\n * @note this method should never throw\n */\n externalHydrationTraversal({\n root,\n outcome,\n recordLookups,\n batches,\n maxPass,\n }: ExternalHydrationWork & {\n root: ExternalHydrationRoot\n outcome: ExternalHydrationOutcome\n }) {\n this.#log('external_hydration_traversal', {\n root,\n outcome,\n recordLookups,\n batches,\n maxPass,\n })\n try {\n this.#externalHydrationLookups.record(recordLookups, { root, outcome })\n this.#externalHydrationBatches.record(batches, { root, outcome })\n } catch (err) {\n diag.error('Failed to record external_hydration_traversal:', err)\n }\n }\n}\n\nexport const events: EventReporter = new EventReporter()\n"]}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Whether the original request `url` is on a configured safe domain. Plain
3
+ * entries match that host exactly; `*.example.com` matches subdomains at any
4
+ * depth but not `example.com` itself. Only credential-free `https:` URLs on
5
+ * the default port qualify. Pure: no DNS, redirects, or record-declared URLs.
6
+ */
7
+ export declare const isExternalRecordUrlSafe: (url: string, domains: readonly string[]) => boolean;
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/util/external/index.ts"],"names":[],"mappings":"AAcA;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAC7B,MAAM,WACF,SAAS,MAAM,EAAE,KACzB,OA6BF,CAAA"}
@@ -0,0 +1,50 @@
1
+ const HOST_LABEL = /^[a-z0-9_]([a-z0-9_-]*[a-z0-9_])?$/;
2
+ const isHostname = (value) => value.split('.').every((label) => HOST_LABEL.test(label));
3
+ const parseDomainPattern = (entry) => {
4
+ const value = entry.trim().toLowerCase();
5
+ const wildcard = value.startsWith('*.');
6
+ const host = wildcard ? value.slice(2) : value;
7
+ return host && isHostname(host) ? { host, wildcard } : undefined;
8
+ };
9
+ /**
10
+ * Whether the original request `url` is on a configured safe domain. Plain
11
+ * entries match that host exactly; `*.example.com` matches subdomains at any
12
+ * depth but not `example.com` itself. Only credential-free `https:` URLs on
13
+ * the default port qualify. Pure: no DNS, redirects, or record-declared URLs.
14
+ */
15
+ export const isExternalRecordUrlSafe = (url, domains) => {
16
+ if (!domains.length)
17
+ return false;
18
+ // `new URL` normalizes backslashes and strips control characters, so
19
+ // reject them up front instead of matching a rewritten URL.
20
+ // eslint-disable-next-line no-control-regex
21
+ if (!/^https:\/\//i.test(url) || /[\\\u0000-\u0020\u007f]/.test(url)) {
22
+ return false;
23
+ }
24
+ let parsed;
25
+ try {
26
+ parsed = new URL(url);
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ if (parsed.protocol !== 'https:' || parsed.port !== '')
32
+ return false;
33
+ if (parsed.username || parsed.password)
34
+ return false;
35
+ // The parser drops an empty userinfo (`https://@host`), so check the raw authority.
36
+ if (/^https:\/\/[^/?#]*@/i.test(url))
37
+ return false;
38
+ const host = parsed.hostname.toLowerCase();
39
+ if (!isHostname(host))
40
+ return false;
41
+ return domains.some((entry) => {
42
+ const pattern = parseDomainPattern(entry);
43
+ if (!pattern)
44
+ return false;
45
+ return pattern.wildcard
46
+ ? host.endsWith(`.${pattern.host}`)
47
+ : host === pattern.host;
48
+ });
49
+ };
50
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/util/external/index.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,GAAG,oCAAoC,CAAA;AAEvD,MAAM,UAAU,GAAG,CAAC,KAAa,EAAW,EAAE,CAC5C,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA;AAI3D,MAAM,kBAAkB,GAAG,CAAC,KAAa,EAA6B,EAAE;IACtE,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;IACxC,MAAM,QAAQ,GAAG,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;IACvC,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAA;IAC9C,OAAO,IAAI,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;AAClE,CAAC,CAAA;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CACrC,GAAW,EACX,OAA0B,EACjB,EAAE;IACX,IAAI,CAAC,OAAO,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACjC,qEAAqE;IACrE,4DAA4D;IAC5D,4CAA4C;IAC5C,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,yBAAyB,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACrE,OAAO,KAAK,CAAA;IACd,CAAC;IAED,IAAI,MAAW,CAAA;IACf,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAA;IACvB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,KAAK,EAAE;QAAE,OAAO,KAAK,CAAA;IACpE,IAAI,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAA;IACpD,oFAAoF;IACpF,IAAI,sBAAsB,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAA;IAElD,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAA;IAC1C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAA;IACnC,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE;QAC5B,MAAM,OAAO,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAA;QACzC,IAAI,CAAC,OAAO;YAAE,OAAO,KAAK,CAAA;QAC1B,OAAO,OAAO,CAAC,QAAQ;YACrB,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;YACnC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,IAAI,CAAA;IAC3B,CAAC,CAAC,CAAA;AACJ,CAAC,CAAA","sourcesContent":["const HOST_LABEL = /^[a-z0-9_]([a-z0-9_-]*[a-z0-9_])?$/\n\nconst isHostname = (value: string): boolean =>\n value.split('.').every((label) => HOST_LABEL.test(label))\n\ntype DomainPattern = { host: string; wildcard: boolean }\n\nconst parseDomainPattern = (entry: string): DomainPattern | undefined => {\n const value = entry.trim().toLowerCase()\n const wildcard = value.startsWith('*.')\n const host = wildcard ? value.slice(2) : value\n return host && isHostname(host) ? { host, wildcard } : undefined\n}\n\n/**\n * Whether the original request `url` is on a configured safe domain. Plain\n * entries match that host exactly; `*.example.com` matches subdomains at any\n * depth but not `example.com` itself. Only credential-free `https:` URLs on\n * the default port qualify. Pure: no DNS, redirects, or record-declared URLs.\n */\nexport const isExternalRecordUrlSafe = (\n url: string,\n domains: readonly string[],\n): boolean => {\n if (!domains.length) return false\n // `new URL` normalizes backslashes and strips control characters, so\n // reject them up front instead of matching a rewritten URL.\n // eslint-disable-next-line no-control-regex\n if (!/^https:\\/\\//i.test(url) || /[\\\\\\u0000-\\u0020\\u007f]/.test(url)) {\n return false\n }\n\n let parsed: URL\n try {\n parsed = new URL(url)\n } catch {\n return false\n }\n if (parsed.protocol !== 'https:' || parsed.port !== '') return false\n if (parsed.username || parsed.password) return false\n // The parser drops an empty userinfo (`https://@host`), so check the raw authority.\n if (/^https:\\/\\/[^/?#]*@/i.test(url)) return false\n\n const host = parsed.hostname.toLowerCase()\n if (!isHostname(host)) return false\n return domains.some((entry) => {\n const pattern = parseDomainPattern(entry)\n if (!pattern) return false\n return pattern.wildcard\n ? host.endsWith(`.${pattern.host}`)\n : host === pattern.host\n })\n}\n"]}
@@ -1,3 +1,5 @@
1
+ import type { AtUriString } from '@atproto/lex';
2
+ import type { AssociatedSiteStandardRecord, ExternalRecords, SiteStandardDocument, SiteStandardPublication } from '../../hydration/external.js';
1
3
  export declare const SITE_STANDARD_NSID_PREFIX = "site.standard.";
2
4
  /**
3
5
  * Confirm that the supplied SS records actually back `assumedUrl`. The
@@ -14,9 +16,11 @@ export declare const SITE_STANDARD_NSID_PREFIX = "site.standard.";
14
16
  * side carries the slash.
15
17
  *
16
18
  * Structural validation of the doc/pub pair (matching `site` ↔ pub URI,
17
- * no orphan docs that claim a missing publication) happens upstream in
18
- * `getSiteStandardRecordsFromHydrationMapsByRefs` /
19
- * `…ByDocumentUri` (see `hydration/external.ts`); by the time this
19
+ * no orphan docs that claim a missing publication) happens upstream: in the
20
+ * pinned `getSiteStandardRecordsFromHydrationMapsByRefs`
21
+ * (`hydration/external.ts`), or by `validateStandardSiteRecordsForUrl` over
22
+ * `getStandardSiteRecordsForUri`'s selection (which alone leaves an orphan
23
+ * document selected with an absent publication). By the time this
20
24
  * function runs the pair is already known to be structurally consistent,
21
25
  * so we only check whether the records back the URL.
22
26
  *
@@ -33,7 +37,7 @@ export declare const SITE_STANDARD_NSID_PREFIX = "site.standard.";
33
37
  * - Loose document (web-URL `site`): `document.site + document.path`
34
38
  * must canonicalize to `assumedUrl` (or be a subpath-friendly prefix).
35
39
  * (Doc with at-uri `site` but no publication can't reach this function
36
- * — the lookups reject it.)
40
+ * — callers reject it.)
37
41
  * - Publication only: `publication.url` must canonicalize exactly to
38
42
  * `assumedUrl`.
39
43
  * - Neither: vacuously valid; the caller short-circuits before we get
@@ -59,6 +63,28 @@ export declare const validateStandardSiteForUrl: (document: {
59
63
  };
60
64
  };
61
65
  } | undefined, assumedUrl: string) => boolean;
66
+ export type StandardSiteRecordSelection = {
67
+ document?: AssociatedSiteStandardRecord<SiteStandardDocument>;
68
+ publication?: AssociatedSiteStandardRecord<SiteStandardPublication>;
69
+ };
70
+ /**
71
+ * Select the Standard.site records backing `canonicalUri` from in-memory
72
+ * hydrated `records`: the canonical record itself and, for a document whose
73
+ * `site` is a publication AT-URI, that publication. Stored bodies are checked
74
+ * against the Lexicon without parsing or defaulting. Nothing is fetched, and a
75
+ * missing or invalid referenced publication leaves that slot empty while the
76
+ * document stays selected.
77
+ */
78
+ export declare const getStandardSiteRecordsForUri: (canonicalUri: AtUriString, records: ExternalRecords | undefined) => StandardSiteRecordSelection;
79
+ /**
80
+ * Whether the selected records form a valid association that backs
81
+ * `assumedUrl`. A document naming a publication AT-URI needs exactly that
82
+ * publication; a supplied publication must be the one the document names.
83
+ * Standalone publications and documents with a web-URL `site` are valid on
84
+ * their own. URL comparison is `validateStandardSiteForUrl`; renderability
85
+ * (e.g. empty titles) is not considered.
86
+ */
87
+ export declare const validateStandardSiteRecordsForUrl: (document: AssociatedSiteStandardRecord<SiteStandardDocument> | undefined, publication: AssociatedSiteStandardRecord<SiteStandardPublication> | undefined, assumedUrl: string) => boolean;
62
88
  /**
63
89
  * Estimate reading time in minutes from a plaintext document body. Returns
64
90
  * `undefined` when the input has no countable words. Uses a coarse
@@ -0,0 +1 @@
1
+ {"version":3,"file":"standard-site.d.ts","sourceRoot":"","sources":["../../../src/util/external/standard-site.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAA4B,MAAM,cAAc,CAAA;AAEzE,OAAO,KAAK,EACV,4BAA4B,EAE5B,eAAe,EACf,oBAAoB,EACpB,uBAAuB,EACxB,MAAM,6BAA6B,CAAA;AAIpC,eAAO,MAAM,yBAAyB,mBAAmB,CAAA;AAgFzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,eAAO,MAAM,0BAA0B,aAEjC;IACE,GAAG,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;IACpB,IAAI,EAAE;QAAE,MAAM,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAA;CAClD,GACD,SAAS,eAEX;IAAE,GAAG,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAAC,IAAI,EAAE;QAAE,MAAM,EAAE;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAA;CAAE,GAAG,SAAS,cAC7D,MAAM,KACjB,OA2BF,CAAA;AAED,MAAM,MAAM,2BAA2B,GAAG;IACxC,QAAQ,CAAC,EAAE,4BAA4B,CAAC,oBAAoB,CAAC,CAAA;IAC7D,WAAW,CAAC,EAAE,4BAA4B,CAAC,uBAAuB,CAAC,CAAA;CACpE,CAAA;AAWD;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,iBACzB,WAAW,WAChB,eAAe,GAAG,SAAS,KACnC,2BAiCF,CAAA;AAUD;;;;;;;GAOG;AACH,eAAO,MAAM,iCAAiC,aAClC,4BAA4B,CAAC,oBAAoB,CAAC,GAAG,SAAS,eAEtE,4BAA4B,CAAC,uBAAuB,CAAC,GAAG,SAAS,cACvD,MAAM,KACjB,OAWF,CAAA;AAID;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,SAC/B,MAAM,KACX,MAAM,GAAG,SAIX,CAAA"}
@@ -1,3 +1,5 @@
1
+ import { parseAtUriString } from '@atproto/syntax';
2
+ import { site } from '../../lexicons/index.js';
1
3
  export const SITE_STANDARD_NSID_PREFIX = 'site.standard.';
2
4
  /**
3
5
  * Parse `url` as HTTP(S) and reduce it to a canonical
@@ -88,9 +90,11 @@ const canonicalUrlMatchesAssumed = (canonicalRecordUrl, canonicalAssumedUrl) =>
88
90
  * side carries the slash.
89
91
  *
90
92
  * Structural validation of the doc/pub pair (matching `site` ↔ pub URI,
91
- * no orphan docs that claim a missing publication) happens upstream in
92
- * `getSiteStandardRecordsFromHydrationMapsByRefs` /
93
- * `…ByDocumentUri` (see `hydration/external.ts`); by the time this
93
+ * no orphan docs that claim a missing publication) happens upstream: in the
94
+ * pinned `getSiteStandardRecordsFromHydrationMapsByRefs`
95
+ * (`hydration/external.ts`), or by `validateStandardSiteRecordsForUrl` over
96
+ * `getStandardSiteRecordsForUri`'s selection (which alone leaves an orphan
97
+ * document selected with an absent publication). By the time this
94
98
  * function runs the pair is already known to be structurally consistent,
95
99
  * so we only check whether the records back the URL.
96
100
  *
@@ -107,7 +111,7 @@ const canonicalUrlMatchesAssumed = (canonicalRecordUrl, canonicalAssumedUrl) =>
107
111
  * - Loose document (web-URL `site`): `document.site + document.path`
108
112
  * must canonicalize to `assumedUrl` (or be a subpath-friendly prefix).
109
113
  * (Doc with at-uri `site` but no publication can't reach this function
110
- * — the lookups reject it.)
114
+ * — callers reject it.)
111
115
  * - Publication only: `publication.url` must canonicalize exactly to
112
116
  * `assumedUrl`.
113
117
  * - Neither: vacuously valid; the caller short-circuits before we get
@@ -133,6 +137,71 @@ export const validateStandardSiteForUrl = (document, publication, assumedUrl) =>
133
137
  }
134
138
  return true;
135
139
  };
140
+ const matchStoredRecord = (schema, uri, info) => info && schema.$matches(info.record, { strict: false })
141
+ ? { ref: { uri, cid: info.cid }, info: { ...info, record: info.record } }
142
+ : undefined;
143
+ /**
144
+ * Select the Standard.site records backing `canonicalUri` from in-memory
145
+ * hydrated `records`: the canonical record itself and, for a document whose
146
+ * `site` is a publication AT-URI, that publication. Stored bodies are checked
147
+ * against the Lexicon without parsing or defaulting. Nothing is fetched, and a
148
+ * missing or invalid referenced publication leaves that slot empty while the
149
+ * document stays selected.
150
+ */
151
+ export const getStandardSiteRecordsForUri = (canonicalUri, records) => {
152
+ const parsed = parseAtUriString(canonicalUri);
153
+ if (!parsed.success)
154
+ return {};
155
+ const info = records?.get(canonicalUri);
156
+ switch (parsed.value.collection) {
157
+ case site.standard.publication.$type: {
158
+ const publication = matchStoredRecord(site.standard.publication.main, canonicalUri, info);
159
+ return publication ? { publication } : {};
160
+ }
161
+ case site.standard.document.$type: {
162
+ const document = matchStoredRecord(site.standard.document.main, canonicalUri, info);
163
+ if (!document)
164
+ return {};
165
+ const publicationUri = publicationUriOf(document.info.record.site);
166
+ if (!publicationUri)
167
+ return { document };
168
+ const publication = matchStoredRecord(site.standard.publication.main, publicationUri, records?.get(publicationUri));
169
+ return publication ? { document, publication } : { document };
170
+ }
171
+ default:
172
+ return {};
173
+ }
174
+ };
175
+ const publicationUriOf = (value) => {
176
+ const parsed = parseAtUriString(value);
177
+ return parsed.success &&
178
+ parsed.value.collection === site.standard.publication.$type
179
+ ? value
180
+ : undefined;
181
+ };
182
+ /**
183
+ * Whether the selected records form a valid association that backs
184
+ * `assumedUrl`. A document naming a publication AT-URI needs exactly that
185
+ * publication; a supplied publication must be the one the document names.
186
+ * Standalone publications and documents with a web-URL `site` are valid on
187
+ * their own. URL comparison is `validateStandardSiteForUrl`; renderability
188
+ * (e.g. empty titles) is not considered.
189
+ */
190
+ export const validateStandardSiteRecordsForUrl = (document, publication, assumedUrl) => {
191
+ if (!document && !publication)
192
+ return false;
193
+ if (document) {
194
+ const documentSite = document.info.record.site;
195
+ if (publication) {
196
+ if (documentSite !== publication.ref.uri)
197
+ return false;
198
+ }
199
+ else if (publicationUriOf(documentSite)) {
200
+ return false;
201
+ }
202
+ }
203
+ return validateStandardSiteForUrl(document, publication, assumedUrl);
204
+ };
136
205
  const WORDS_PER_MINUTE = 200;
137
206
  /**
138
207
  * Estimate reading time in minutes from a plaintext document body. Returns
@@ -0,0 +1 @@
1
+ {"version":3,"file":"standard-site.js","sourceRoot":"","sources":["../../../src/util/external/standard-site.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AtUriString, InferInput, RecordSchema } from '@atproto/lex'\nimport { parseAtUriString } from '@atproto/syntax'\nimport type {\n AssociatedSiteStandardRecord,\n ExternalRecord,\n ExternalRecords,\n SiteStandardDocument,\n SiteStandardPublication,\n} from '../../hydration/external.js'\nimport type { RecordInfo } from '../../hydration/util.js'\nimport { site } from '../../lexicons/index.js'\n\nexport const SITE_STANDARD_NSID_PREFIX = 'site.standard.'\n\n/**\n * Parse `url` as HTTP(S) and reduce it to a canonical\n * `protocol://host/path` string for equality comparison: lowercases host,\n * strips a trailing slash from the path, and drops query/fragment. Returns\n * `null` when the input isn't a valid HTTP(S) URL.\n */\nconst canonicalizeHttpUrl = (url: string): string | null => {\n let parsed: URL\n try {\n parsed = new URL(url)\n } catch {\n return null\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return null\n const path = parsed.pathname === '/' ? '' : parsed.pathname.replace(/\\/$/, '')\n return `${parsed.protocol}//${parsed.host}${path}`\n}\n\n/**\n * Append `path` to `base` with exactly one slash between, or return `base`\n * unchanged when `path` is empty. Unlike `new URL(path, base)`, a leading\n * slash on `path` does NOT swallow `base`'s pathname — so\n * `joinPath('https://x.com/blog', '/foo')` is `https://x.com/blog/foo`,\n * not `https://x.com/foo`.\n */\nconst joinPath = (base: string, path: string): string => {\n if (!path) return base\n const baseTrimmed = base.endsWith('/') ? base.slice(0, -1) : base\n const pathTrimmed = path.startsWith('/') ? path.slice(1) : path\n return `${baseTrimmed}/${pathTrimmed}`\n}\n\n/**\n * Apex domains whose authors own the full subpath space under their\n * record-claimed URL. Each entry matches itself and any subdomain; e.g.\n * `pckt.blog` matches `pckt.blog` and `waow-tech.pckt.blog`.\n *\n * On these domains, an `assumedUrl` whose pathname extends the canonical\n * record URL with extra segments is treated as valid. Adding to this list\n * is a trust call — only platforms where each record's URL space is\n * authoritatively owned by its author belong here.\n */\nconst SUBPATH_FRIENDLY_DOMAINS = ['pckt.blog', 'leaflet.pub', 'offprint.app']\n\nconst isSubpathFriendlyHost = (host: string): boolean => {\n const lower = host.toLowerCase()\n return SUBPATH_FRIENDLY_DOMAINS.some(\n (domain) => lower === domain || lower.endsWith(`.${domain}`),\n )\n}\n\n/**\n * Return whether `recordUrl` and `assumedUrl` should validate as the same\n * canonical content. Strictly equal canonical forms always match. On\n * subpath-friendly hosts (see `SUBPATH_FRIENDLY_DOMAINS`), an `assumedUrl`\n * whose path extends `recordUrl`'s with extra segments is also accepted.\n *\n * Both inputs are pre-canonicalized strings (`canonicalizeHttpUrl` output)\n * with no trailing slash and no query/fragment.\n */\nconst canonicalUrlMatchesAssumed = (\n canonicalRecordUrl: string,\n canonicalAssumedUrl: string,\n): boolean => {\n if (canonicalRecordUrl === canonicalAssumedUrl) return true\n // Subpath fallback. Both strings are canonicalized, so a real\n // path-segment boundary at `recordUrl + '/'` (e.g. `/foo` vs `/foo-bar`\n // never matches; `/foo` vs `/foo/bar` does).\n if (!canonicalAssumedUrl.startsWith(`${canonicalRecordUrl}/`)) return false\n let host: string\n try {\n host = new URL(canonicalAssumedUrl).host\n } catch {\n return false\n }\n return isSubpathFriendlyHost(host)\n}\n\n/**\n * Confirm that the supplied SS records actually back `assumedUrl`. The\n * record-side URL is built by concatenating the publication URL (or the\n * loose-doc site) with the document's `path` field, then both sides are\n * canonicalized for equality: lowercase host, query/fragment dropped,\n * trailing slash stripped.\n *\n * Path concatenation is `base + '/' + path` semantics — a leading `/` on\n * `path` does NOT swallow the base's pathname (the way\n * `new URL(path, base)` would). So\n * `'https://atproto.com/blog' + '/indexing-standard-site'` resolves to\n * `https://atproto.com/blog/indexing-standard-site` regardless of which\n * side carries the slash.\n *\n * Structural validation of the doc/pub pair (matching `site` ↔ pub URI,\n * no orphan docs that claim a missing publication) happens upstream: in the\n * pinned `getSiteStandardRecordsFromHydrationMapsByRefs`\n * (`hydration/external.ts`), or by `validateStandardSiteRecordsForUrl` over\n * `getStandardSiteRecordsForUri`'s selection (which alone leaves an orphan\n * document selected with an absent publication). By the time this\n * function runs the pair is already known to be structurally consistent,\n * so we only check whether the records back the URL.\n *\n * For document validation, `SUBPATH_FRIENDLY_DOMAINS` (and their\n * subdomains) accept an assumed URL whose path extends the canonical\n * record URL with additional segments — these are platforms where each\n * record's author owns the full subpath space under their claimed URL.\n * Publication-only validation always requires exact match: there's no\n * coherent \"subpath\" of a publication's home page.\n *\n * Cases:\n * - Document + publication: `publication.url + document.path` must\n * canonicalize to `assumedUrl` (or be a subpath-friendly prefix of it).\n * - Loose document (web-URL `site`): `document.site + document.path`\n * must canonicalize to `assumedUrl` (or be a subpath-friendly prefix).\n * (Doc with at-uri `site` but no publication can't reach this function\n * — callers reject it.)\n * - Publication only: `publication.url` must canonicalize exactly to\n * `assumedUrl`.\n * - Neither: vacuously valid; the caller short-circuits before we get\n * here.\n */\nexport const validateStandardSiteForUrl = (\n document:\n | {\n ref: { uri: string }\n info: { record: { site: string; path?: string } }\n }\n | undefined,\n publication:\n { ref: { uri: string }; info: { record: { url: string } } } | undefined,\n assumedUrl: string,\n): boolean => {\n const canonicalAssumed = canonicalizeHttpUrl(assumedUrl)\n if (canonicalAssumed === null) return false\n\n if (document && publication) {\n const joined = canonicalizeHttpUrl(\n joinPath(publication.info.record.url, document.info.record.path ?? ''),\n )\n return (\n joined !== null && canonicalUrlMatchesAssumed(joined, canonicalAssumed)\n )\n }\n if (document) {\n const joined = canonicalizeHttpUrl(\n joinPath(document.info.record.site, document.info.record.path ?? ''),\n )\n return (\n joined !== null && canonicalUrlMatchesAssumed(joined, canonicalAssumed)\n )\n }\n if (publication) {\n // Publication-only matches are exact: `assumedUrl` represents the\n // publication's home page, not an article underneath it. Subpath\n // extensions belong to document validation.\n return canonicalizeHttpUrl(publication.info.record.url) === canonicalAssumed\n }\n return true\n}\n\nexport type StandardSiteRecordSelection = {\n document?: AssociatedSiteStandardRecord<SiteStandardDocument>\n publication?: AssociatedSiteStandardRecord<SiteStandardPublication>\n}\n\nconst matchStoredRecord = <TSchema extends RecordSchema>(\n schema: TSchema,\n uri: AtUriString,\n info: ExternalRecord | null | undefined,\n): AssociatedSiteStandardRecord<RecordInfo<InferInput<TSchema>>> | undefined =>\n info && schema.$matches(info.record, { strict: false })\n ? { ref: { uri, cid: info.cid }, info: { ...info, record: info.record } }\n : undefined\n\n/**\n * Select the Standard.site records backing `canonicalUri` from in-memory\n * hydrated `records`: the canonical record itself and, for a document whose\n * `site` is a publication AT-URI, that publication. Stored bodies are checked\n * against the Lexicon without parsing or defaulting. Nothing is fetched, and a\n * missing or invalid referenced publication leaves that slot empty while the\n * document stays selected.\n */\nexport const getStandardSiteRecordsForUri = (\n canonicalUri: AtUriString,\n records: ExternalRecords | undefined,\n): StandardSiteRecordSelection => {\n const parsed = parseAtUriString(canonicalUri)\n if (!parsed.success) return {}\n const info = records?.get(canonicalUri)\n\n switch (parsed.value.collection) {\n case site.standard.publication.$type: {\n const publication = matchStoredRecord(\n site.standard.publication.main,\n canonicalUri,\n info,\n )\n return publication ? { publication } : {}\n }\n case site.standard.document.$type: {\n const document = matchStoredRecord(\n site.standard.document.main,\n canonicalUri,\n info,\n )\n if (!document) return {}\n const publicationUri = publicationUriOf(document.info.record.site)\n if (!publicationUri) return { document }\n const publication = matchStoredRecord(\n site.standard.publication.main,\n publicationUri,\n records?.get(publicationUri),\n )\n return publication ? { document, publication } : { document }\n }\n default:\n return {}\n }\n}\n\nconst publicationUriOf = (value: string): AtUriString | undefined => {\n const parsed = parseAtUriString(value)\n return parsed.success &&\n parsed.value.collection === site.standard.publication.$type\n ? (value as AtUriString)\n : undefined\n}\n\n/**\n * Whether the selected records form a valid association that backs\n * `assumedUrl`. A document naming a publication AT-URI needs exactly that\n * publication; a supplied publication must be the one the document names.\n * Standalone publications and documents with a web-URL `site` are valid on\n * their own. URL comparison is `validateStandardSiteForUrl`; renderability\n * (e.g. empty titles) is not considered.\n */\nexport const validateStandardSiteRecordsForUrl = (\n document: AssociatedSiteStandardRecord<SiteStandardDocument> | undefined,\n publication:\n AssociatedSiteStandardRecord<SiteStandardPublication> | undefined,\n assumedUrl: string,\n): boolean => {\n if (!document && !publication) return false\n if (document) {\n const documentSite = document.info.record.site\n if (publication) {\n if (documentSite !== publication.ref.uri) return false\n } else if (publicationUriOf(documentSite)) {\n return false\n }\n }\n return validateStandardSiteForUrl(document, publication, assumedUrl)\n}\n\nconst WORDS_PER_MINUTE = 200\n\n/**\n * Estimate reading time in minutes from a plaintext document body. Returns\n * `undefined` when the input has no countable words. Uses a coarse\n * `WORDS_PER_MINUTE` heuristic; swap in a more accurate library here if\n * needed (e.g. `reading-time`).\n */\nexport const estimateReadingTimeMinutes = (\n text: string,\n): number | undefined => {\n const words = text.trim().split(/\\s+/).filter(Boolean).length\n if (!words) return undefined\n return Math.max(1, Math.ceil(words / WORDS_PER_MINUTE))\n}\n"]}
@@ -2,7 +2,7 @@ import { MINUTE } from '@atproto/common';
2
2
  import { getBlobCidString, isAtUriString, isDidString, } from '@atproto/lex';
3
3
  import { AtUri, normalizeDatetimeAlways } from '@atproto/syntax';
4
4
  import { app, com, place, site, social } from '../lexicons/index.js';
5
- import { estimateReadingTimeMinutes } from '../util/standard-site.js';
5
+ import { estimateReadingTimeMinutes } from '../util/external/standard-site.js';
6
6
  import { uriToDid } from '../util/uris.js';
7
7
  export const LIVESTREAM_HEARTBEAT_WINDOW_MS = 2 * MINUTE;
8
8
  /** Build a modality view from validated, available records in hydration state. */
@@ -1 +1 @@
1
- {"version":3,"file":"external.js","sourceRoot":"","sources":["../../src/views/external.ts"],"names":[],"mappings":"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","sourcesContent":["import { MINUTE } from '@atproto/common'\nimport {\n type $Typed,\n type AtUriString,\n type DidString,\n type NsidString,\n type UriString,\n getBlobCidString,\n isAtUriString,\n isDidString,\n} from '@atproto/lex'\nimport { AtUri, normalizeDatetimeAlways } from '@atproto/syntax'\nimport type { ExternalRecord } from '../hydration/external.js'\nimport type { HydrationState } from '../hydration/hydrator.js'\nimport { app, com, place, site, social } from '../lexicons/index.js'\nimport { estimateReadingTimeMinutes } from '../util/standard-site.js'\nimport { uriToDid } from '../util/uris.js'\nimport type { Views } from './index.js'\nimport type { ExternalRecordView, Label, ProfileViewBasic } from './types.js'\n\nexport const LIVESTREAM_HEARTBEAT_WINDOW_MS = 2 * MINUTE\n\n/** Build a modality view from validated, available records in hydration state. */\nexport function externalRecordView(\n views: Views,\n uri: AtUriString,\n state: HydrationState,\n now: number,\n): $Typed<ExternalRecordView> | undefined {\n const info = availableRecord(views, uri, state)\n if (!info) return\n const { record } = info\n\n if (site.standard.document.$matches(record)) {\n let publisher:\n $Typed<app.bsky.embed.external.ViewArticlePublication> | undefined\n let base = record.site\n const refs = [uri]\n if (isAtUriString(record.site)) {\n publisher = publicationView(views, record.site, state)\n if (!publisher) return\n base = publisher.uri\n refs.push(record.site)\n }\n const url = httpUri(\n record.path\n ? `${base.replace(/\\/$/, '')}/${record.path.replace(/^\\//, '')}`\n : base,\n )\n if (!url || !record.title) return\n return app.bsky.embed.external.viewArticle.$build({\n ...commonFields(views, refs, state),\n uri: url,\n title: record.title,\n description: record.description ?? '',\n createdAt: record.publishedAt,\n updatedAt: record.updatedAt,\n image: record.coverImage\n ? views.imgUriBuilder.getPresetUri(\n 'feed_thumbnail',\n uriToDid(uri),\n getBlobCidString(record.coverImage),\n )\n : undefined,\n publisher,\n readingTime: record.textContent\n ? estimateReadingTimeMinutes(record.textContent)\n : undefined,\n likeCount: backlinkCount(uri, site.standard.graph.recommend.$type, state),\n likers: backlinkProfiles(views, uri, state, 'recommend'),\n })\n }\n\n if (site.standard.publication.$matches(record)) {\n return publicationView(views, uri, state)\n }\n\n if (social.grain.gallery.$matches(record)) {\n const links = (state.externalRecordBacklinks?.get(uri) ?? []).flatMap(\n (linkUri) => {\n const link = availableRecord(views, linkUri, state)?.record\n if (\n !social.grain.gallery.item.$matches(link) ||\n link.gallery !== uri ||\n uriToDid(linkUri) !== uriToDid(uri)\n ) {\n return []\n }\n return [{ uri: linkUri, record: link }]\n },\n )\n links.sort(\n (a, b) =>\n (a.record.position ?? 0) - (b.record.position ?? 0) ||\n compareStrings(a.uri, b.uri),\n )\n\n const refs = [uri]\n const seenPhotos = new Set<AtUriString>()\n const items: $Typed<app.bsky.embed.external.ViewGalleryImage>[] = []\n for (const link of links) {\n const photoUri = link.record.item\n if (seenPhotos.has(photoUri)) continue\n const photo = availableRecord(views, photoUri, state)?.record\n if (!social.grain.photo.$matches(photo)) continue\n seenPhotos.add(photoUri)\n refs.push(link.uri, photoUri)\n items.push(\n app.bsky.embed.external.viewGalleryImage.$build({\n thumbnail: views.imgUriBuilder.getPresetUri(\n 'feed_thumbnail',\n uriToDid(photoUri),\n getBlobCidString(photo.photo),\n ),\n fullsize: views.imgUriBuilder.getPresetUri(\n 'feed_fullsize',\n uriToDid(photoUri),\n getBlobCidString(photo.photo),\n ),\n alt: photo.alt,\n aspectRatio: photo.aspectRatio\n ? {\n width: photo.aspectRatio.width,\n height: photo.aspectRatio.height,\n }\n : undefined,\n }),\n )\n }\n return app.bsky.embed.external.viewGallery.$build({\n ...commonFields(views, refs, state),\n uri,\n title: record.title,\n description: record.description ?? '',\n createdAt: record.createdAt,\n items,\n likeCount: backlinkCount(uri, social.grain.favorite.$type, state),\n likers: backlinkProfiles(views, uri, state, 'favorite'),\n })\n }\n\n if (place.stream.livestream.$matches(record)) {\n const lastSeen = record.lastSeenAt ? Date.parse(record.lastSeenAt) : NaN\n // @NOTE The window applies on both sides of now to tolerate clock skew, so\n // a future heartbeat counts as live for at most twice the window.\n const fresh = Math.abs(now - lastSeen) < LIVESTREAM_HEARTBEAT_WINDOW_MS\n return app.bsky.embed.external.viewLivestream.$build({\n ...commonFields(views, [uri], state),\n uri: httpUri(record.canonicalUrl) ?? httpUri(record.url) ?? uri,\n title: record.title,\n description: '',\n createdAt: record.createdAt,\n image: record.thumb\n ? views.imgUriBuilder.getPresetUri(\n 'feed_thumbnail',\n uriToDid(uri),\n getBlobCidString(record.thumb),\n )\n : undefined,\n active: !record.endedAt && fresh,\n startedAt: record.createdAt,\n endedAt: record.endedAt,\n })\n }\n}\n\nfunction publicationView(\n views: Views,\n uri: AtUriString,\n state: HydrationState,\n): $Typed<app.bsky.embed.external.ViewArticlePublication> | undefined {\n const record = availableRecord(views, uri, state)?.record\n if (!site.standard.publication.$matches(record)) return\n const url = httpUri(record.url)\n if (!url || !record.name) return\n return app.bsky.embed.external.viewArticlePublication.$build({\n ...commonFields(views, [uri], state),\n uri: url,\n title: record.name,\n description: record.description ?? '',\n logo: record.icon\n ? views.imgUriBuilder.getPresetUri(\n 'avatar',\n uriToDid(uri),\n getBlobCidString(record.icon),\n )\n : undefined,\n theme: record.basicTheme\n ? app.bsky.embed.external.viewArticlePublicationTheme.$build({\n background: hexColor(record.basicTheme.background),\n foreground: hexColor(record.basicTheme.foreground),\n accent: hexColor(record.basicTheme.accent),\n accentForeground: hexColor(record.basicTheme.accentForeground),\n })\n : undefined,\n subscriptionCount: backlinkCount(\n uri,\n site.standard.graph.subscription.$type,\n state,\n ),\n subscribers: backlinkProfiles(views, uri, state, 'subscription'),\n })\n}\n\nfunction availableRecord(\n views: Views,\n uri: AtUriString,\n state: HydrationState,\n): ExternalRecord | undefined {\n const info = state.externalRecords?.get(uri)\n if (!info) return\n const parsed = new AtUri(uri)\n if (!isDidString(parsed.host) || parsed.collection !== info.record.$type)\n return\n // @NOTE Actor hydration nulls unavailable accounts (missing and tombstoned\n // ones even with includeTakedowns). Absent entries just weren't hydrated.\n if (state.actors?.get(parsed.host) === null) return\n if (views.viewerBlockExists(parsed.host, state)) return\n if (!views.viewerSeesNeedsReview({ uri }, state)) return\n if (\n !state.ctx?.includeTakedowns &&\n (info.takedownRef ||\n state.labels?.get(uri)?.isTakendown ||\n views.actorIsNoHosted(parsed.host, state))\n ) {\n return\n }\n return info\n}\n\nfunction commonFields(\n views: Views,\n uris: AtUriString[],\n state: HydrationState,\n) {\n const refs = [...new Set(uris)]\n const dids = [...new Set(refs.map(uriToDid))]\n return {\n associatedRefs: refs.map((uri) =>\n com.atproto.repo.strongRef.$build({\n uri,\n cid: state.externalRecords!.get(uri)!.cid,\n }),\n ),\n associatedProfiles: dids.flatMap((did) => {\n const profile = views.profileBasic(did, state)\n return profile ? [profile] : []\n }),\n labels: refs.flatMap((uri) =>\n recordLabels(uri, state.externalRecords!.get(uri)!, state),\n ),\n }\n}\n\nfunction recordLabels(\n uri: AtUriString,\n info: ExternalRecord,\n state: HydrationState,\n): Label[] {\n const labels = state.labels?.getBySubject(uri) ?? []\n const selfLabels = info.record.labels\n if (!com.atproto.label.defs.selfLabels.$matches(selfLabels)) return labels\n const createdAt = info.record.createdAt ?? info.record.publishedAt\n const cts =\n typeof createdAt === 'string'\n ? normalizeDatetimeAlways(createdAt)\n : '1970-01-01T00:00:00.000Z'\n return [\n ...labels,\n ...selfLabels.values.map(({ val }) => ({\n src: uriToDid(uri),\n uri,\n cid: info.cid,\n val,\n cts,\n })),\n ]\n}\n\nfunction backlinkCount(\n uri: AtUriString,\n collection: NsidString,\n state: HydrationState,\n): number | undefined {\n const counts = state.externalRecordBacklinkCounts?.get(uri)\n if (!counts) return\n const count = counts[collection] ?? 0\n return Number.isSafeInteger(count) && count >= 0 ? count : undefined\n}\n\nfunction backlinkProfiles(\n views: Views,\n uri: AtUriString,\n state: HydrationState,\n kind: 'recommend' | 'subscription' | 'favorite',\n): ProfileViewBasic[] | undefined {\n const backlinks = state.externalRecordBacklinks?.get(uri)\n if (!backlinks) return\n const dids = new Set<DidString>()\n for (const backlink of backlinks) {\n const record = availableRecord(views, backlink, state)?.record\n const matches =\n kind === 'recommend'\n ? site.standard.graph.recommend.$matches(record) &&\n record.document === uri\n : kind === 'subscription'\n ? site.standard.graph.subscription.$matches(record) &&\n record.publication === uri\n : social.grain.favorite.$matches(record) && record.subject === uri\n if (matches) dids.add(uriToDid(backlink))\n }\n const profiles: ProfileViewBasic[] = []\n for (const did of [...dids].sort(compareStrings)) {\n const profile = views.profileBasic(did, state)\n if (profile) profiles.push(profile)\n if (profiles.length === 3) break\n }\n return profiles\n}\n\nfunction hexColor(\n color: site.standard.theme.basic.Main['background'],\n): string | undefined {\n if (site.standard.theme.color.rgb.$matches(color)) {\n return `#${[color.r, color.g, color.b].map(hexByte).join('')}`\n }\n}\n\nfunction hexByte(value: number): string {\n return value.toString(16).padStart(2, '0')\n}\n\nfunction httpUri(value: string | undefined): UriString | undefined {\n if (!value) return\n try {\n const url = new URL(value)\n if (url.protocol === 'http:' || url.protocol === 'https:')\n return url.href as UriString\n } catch {\n return\n }\n}\n\nfunction compareStrings(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0\n}\n"]}
1
+ {"version":3,"file":"external.js","sourceRoot":"","sources":["../../src/views/external.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AACxC,OAAO,EAML,gBAAgB,EAChB,aAAa,EACb,WAAW,GACZ,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,KAAK,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAA;AAGhE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AACpE,OAAO,EAAE,0BAA0B,EAAE,MAAM,mCAAmC,CAAA;AAC9E,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAI1C,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAAC,GAAG,MAAM,CAAA;AAExD,kFAAkF;AAClF,MAAM,UAAU,kBAAkB,CAChC,KAAY,EACZ,GAAgB,EAChB,KAAqB,EACrB,GAAW;IAEX,MAAM,IAAI,GAAG,eAAe,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,CAAA;IAC/C,IAAI,CAAC,IAAI;QAAE,OAAM;IACjB,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;IAEvB,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5C,IAAI,SACgE,CAAA;QACpE,IAAI,IAAI,GAAG,MAAM,CAAC,IAAI,CAAA;QACtB,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,CAAA;QAClB,IAAI,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;YAC/B,SAAS,GAAG,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;YACtD,IAAI,CAAC,SAAS;gBAAE,OAAM;YACtB,IAAI,GAAG,SAAS,CAAC,GAAG,CAAA;YACpB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QACxB,CAAC;QACD,MAAM,GAAG,GAAG,OAAO,CACjB,MAAM,CAAC,IAAI;YACT,CAAC,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE;YAChE,CAAC,CAAC,IAAI,CACT,CAAA;QACD,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK;YAAE,OAAM;QACjC,OAAO,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,MAAM,CAAC;YAChD,GAAG,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC;YACnC,GAAG,EAAE,GAAG;YACR,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,WAAW,EAAE,MAAM,CAAC,WAAW,IAAI,EAAE;YACrC,SAAS,EAAE,MAAM,CAAC,WAAW;YAC7B,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,KAAK,EAAE,MAAM,CAAC,UAAU;gBACtB,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,YAAY,CAC9B,gBAAgB,EAChB,QAAQ,CAAC,GAAG,CAAC,EACb,gBAAgB,CAAC,MAAM,CAAC,UAAU,CAAC,CACpC;gBACH,CAAC,CAAC,SAAS;YACb,SAAS;YACT,WAAW,EAAE,MAAM,CAAC,WAAW;gBAC7B,CAAC,CAAC,0BAA0B,CAAC,MAAM,CAAC,WAAW,CAAC;gBAChD,CAAC,CAAC,SAAS;YACb,SAAS,EAAE,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC;YACzE,MAAM,EAAE,gBAAgB,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,WAAW,CAAC;SACzD,CAAC,CAAA;IACJ,CAAC;IAED,IAAI,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC/C,OAAO,eAAe,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,CAAA;IAC3C,CAAC;IAED,IAAI,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1C,MAAM,KAAK,GAAG,CAAC,KAAK,CAAC,uBAAuB,EAAE,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CACnE,CAAC,OAAO,EAAE,EAAE;YACV,MAAM,IAAI,GAAG,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,CAAA;YAC3D,IACE,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;gBACzC,IAAI,CAAC,OAAO,KAAK,GAAG;gBACpB,QAAQ,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,GAAG,CAAC,EACnC,CAAC;gBACD,OAAO,EAAE,CAAA;YACX,CAAC;YACD,OAAO,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;QACzC,CAAC,CACF,CAAA;QACD,KAAK,CAAC,IAAI,CACR,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CACP,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAC;YACnD,cAAc,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,CAC/B,CAAA;QAED,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,CAAA;QAClB,MAAM,UAAU,GAAG,IAAI,GAAG,EAAe,CAAA;QACzC,MAAM,KAAK,GAAuD,EAAE,CAAA;QACpE,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAA;YACjC,IAAI,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC;gBAAE,SAAQ;YACtC,MAAM,KAAK,GAAG,eAAe,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,CAAC,EAAE,MAAM,CAAA;YAC7D,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC;gBAAE,SAAQ;YACjD,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;YACxB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAA;YAC7B,KAAK,CAAC,IAAI,CACR,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,gBAAgB,CAAC,MAAM,CAAC;gBAC9C,SAAS,EAAE,KAAK,CAAC,aAAa,CAAC,YAAY,CACzC,gBAAgB,EAChB,QAAQ,CAAC,QAAQ,CAAC,EAClB,gBAAgB,CAAC,KAAK,CAAC,KAAK,CAAC,CAC9B;gBACD,QAAQ,EAAE,KAAK,CAAC,aAAa,CAAC,YAAY,CACxC,eAAe,EACf,QAAQ,CAAC,QAAQ,CAAC,EAClB,gBAAgB,CAAC,KAAK,CAAC,KAAK,CAAC,CAC9B;gBACD,GAAG,EAAE,KAAK,CAAC,GAAG;gBACd,WAAW,EAAE,KAAK,CAAC,WAAW;oBAC5B,CAAC,CAAC;wBACE,KAAK,EAAE,KAAK,CAAC,WAAW,CAAC,KAAK;wBAC9B,MAAM,EAAE,KAAK,CAAC,WAAW,CAAC,MAAM;qBACjC;oBACH,CAAC,CAAC,SAAS;aACd,CAAC,CACH,CAAA;QACH,CAAC;QACD,OAAO,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,MAAM,CAAC;YAChD,GAAG,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC;YACnC,GAAG;YACH,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,WAAW,EAAE,MAAM,CAAC,WAAW,IAAI,EAAE;YACrC,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,KAAK;YACL,SAAS,EAAE,aAAa,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;YACjE,MAAM,EAAE,gBAAgB,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,UAAU,CAAC;SACxD,CAAC,CAAA;IACJ,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC7C,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,CAAA;QACxE,2EAA2E;QAC3E,kEAAkE;QAClE,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,QAAQ,CAAC,GAAG,8BAA8B,CAAA;QACvE,OAAO,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC;YACnD,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC;YACpC,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,GAAG;YAC/D,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,WAAW,EAAE,EAAE;YACf,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,KAAK,EAAE,MAAM,CAAC,KAAK;gBACjB,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,YAAY,CAC9B,gBAAgB,EAChB,QAAQ,CAAC,GAAG,CAAC,EACb,gBAAgB,CAAC,MAAM,CAAC,KAAK,CAAC,CAC/B;gBACH,CAAC,CAAC,SAAS;YACb,MAAM,EAAE,CAAC,MAAM,CAAC,OAAO,IAAI,KAAK;YAChC,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,OAAO,EAAE,MAAM,CAAC,OAAO;SACxB,CAAC,CAAA;IACJ,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CACtB,KAAY,EACZ,GAAgB,EAChB,KAAqB;IAErB,MAAM,MAAM,GAAG,eAAe,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,MAAM,CAAA;IACzD,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAM;IACvD,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IAC/B,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI;QAAE,OAAM;IAChC,OAAO,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,sBAAsB,CAAC,MAAM,CAAC;QAC3D,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC;QACpC,GAAG,EAAE,GAAG;QACR,KAAK,EAAE,MAAM,CAAC,IAAI;QAClB,WAAW,EAAE,MAAM,CAAC,WAAW,IAAI,EAAE;QACrC,IAAI,EAAE,MAAM,CAAC,IAAI;YACf,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,YAAY,CAC9B,QAAQ,EACR,QAAQ,CAAC,GAAG,CAAC,EACb,gBAAgB,CAAC,MAAM,CAAC,IAAI,CAAC,CAC9B;YACH,CAAC,CAAC,SAAS;QACb,KAAK,EAAE,MAAM,CAAC,UAAU;YACtB,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,2BAA2B,CAAC,MAAM,CAAC;gBACzD,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC;gBAClD,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC;gBAClD,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC;gBAC1C,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,gBAAgB,CAAC;aAC/D,CAAC;YACJ,CAAC,CAAC,SAAS;QACb,iBAAiB,EAAE,aAAa,CAC9B,GAAG,EACH,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,EACtC,KAAK,CACN;QACD,WAAW,EAAE,gBAAgB,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,cAAc,CAAC;KACjE,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,eAAe,CACtB,KAAY,EACZ,GAAgB,EAChB,KAAqB;IAErB,MAAM,IAAI,GAAG,KAAK,CAAC,eAAe,EAAE,GAAG,CAAC,GAAG,CAAC,CAAA;IAC5C,IAAI,CAAC,IAAI;QAAE,OAAM;IACjB,MAAM,MAAM,GAAG,IAAI,KAAK,CAAC,GAAG,CAAC,CAAA;IAC7B,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,UAAU,KAAK,IAAI,CAAC,MAAM,CAAC,KAAK;QACtE,OAAM;IACR,2EAA2E;IAC3E,0EAA0E;IAC1E,IAAI,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI;QAAE,OAAM;IACnD,IAAI,KAAK,CAAC,iBAAiB,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC;QAAE,OAAM;IACvD,IAAI,CAAC,KAAK,CAAC,qBAAqB,CAAC,EAAE,GAAG,EAAE,EAAE,KAAK,CAAC;QAAE,OAAM;IACxD,IACE,CAAC,KAAK,CAAC,GAAG,EAAE,gBAAgB;QAC5B,CAAC,IAAI,CAAC,WAAW;YACf,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,EAAE,WAAW;YACnC,KAAK,CAAC,eAAe,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,EAC5C,CAAC;QACD,OAAM;IACR,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,SAAS,YAAY,CACnB,KAAY,EACZ,IAAmB,EACnB,KAAqB;IAErB,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,CAAA;IAC/B,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAA;IAC7C,OAAO;QACL,cAAc,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAC/B,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;YAChC,GAAG;YACH,GAAG,EAAE,KAAK,CAAC,eAAgB,CAAC,GAAG,CAAC,GAAG,CAAE,CAAC,GAAG;SAC1C,CAAC,CACH;QACD,kBAAkB,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;YACvC,MAAM,OAAO,GAAG,KAAK,CAAC,YAAY,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;YAC9C,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;QACjC,CAAC,CAAC;QACF,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAC3B,YAAY,CAAC,GAAG,EAAE,KAAK,CAAC,eAAgB,CAAC,GAAG,CAAC,GAAG,CAAE,EAAE,KAAK,CAAC,CAC3D;KACF,CAAA;AACH,CAAC;AAED,SAAS,YAAY,CACnB,GAAgB,EAChB,IAAoB,EACpB,KAAqB;IAErB,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,EAAE,YAAY,CAAC,GAAG,CAAC,IAAI,EAAE,CAAA;IACpD,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAA;IACrC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,UAAU,CAAC;QAAE,OAAO,MAAM,CAAA;IAC1E,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,IAAI,CAAC,MAAM,CAAC,WAAW,CAAA;IAClE,MAAM,GAAG,GACP,OAAO,SAAS,KAAK,QAAQ;QAC3B,CAAC,CAAC,uBAAuB,CAAC,SAAS,CAAC;QACpC,CAAC,CAAC,0BAA0B,CAAA;IAChC,OAAO;QACL,GAAG,MAAM;QACT,GAAG,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC;YACrC,GAAG,EAAE,QAAQ,CAAC,GAAG,CAAC;YAClB,GAAG;YACH,GAAG,EAAE,IAAI,CAAC,GAAG;YACb,GAAG;YACH,GAAG;SACJ,CAAC,CAAC;KACJ,CAAA;AACH,CAAC;AAED,SAAS,aAAa,CACpB,GAAgB,EAChB,UAAsB,EACtB,KAAqB;IAErB,MAAM,MAAM,GAAG,KAAK,CAAC,4BAA4B,EAAE,GAAG,CAAC,GAAG,CAAC,CAAA;IAC3D,IAAI,CAAC,MAAM;QAAE,OAAM;IACnB,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;IACrC,OAAO,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAA;AACtE,CAAC;AAED,SAAS,gBAAgB,CACvB,KAAY,EACZ,GAAgB,EAChB,KAAqB,EACrB,IAA+C;IAE/C,MAAM,SAAS,GAAG,KAAK,CAAC,uBAAuB,EAAE,GAAG,CAAC,GAAG,CAAC,CAAA;IACzD,IAAI,CAAC,SAAS;QAAE,OAAM;IACtB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAa,CAAA;IACjC,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,MAAM,MAAM,GAAG,eAAe,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,CAAC,EAAE,MAAM,CAAA;QAC9D,MAAM,OAAO,GACX,IAAI,KAAK,WAAW;YAClB,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC;gBAC9C,MAAM,CAAC,QAAQ,KAAK,GAAG;YACzB,CAAC,CAAC,IAAI,KAAK,cAAc;gBACvB,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,CAAC,MAAM,CAAC;oBACjD,MAAM,CAAC,WAAW,KAAK,GAAG;gBAC5B,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,KAAK,GAAG,CAAA;QACxE,IAAI,OAAO;YAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC3C,CAAC;IACD,MAAM,QAAQ,GAAuB,EAAE,CAAA;IACvC,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC;QACjD,MAAM,OAAO,GAAG,KAAK,CAAC,YAAY,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;QAC9C,IAAI,OAAO;YAAE,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACnC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,MAAK;IAClC,CAAC;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED,SAAS,QAAQ,CACf,KAAmD;IAEnD,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAClD,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAA;IAChE,CAAC;AACH,CAAC;AAED,SAAS,OAAO,CAAC,KAAa;IAC5B,OAAO,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;AAC5C,CAAC;AAED,SAAS,OAAO,CAAC,KAAyB;IACxC,IAAI,CAAC,KAAK;QAAE,OAAM;IAClB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAA;QAC1B,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ;YACvD,OAAO,GAAG,CAAC,IAAiB,CAAA;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,OAAM;IACR,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,CAAS,EAAE,CAAS;IAC1C,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AACnC,CAAC","sourcesContent":["import { MINUTE } from '@atproto/common'\nimport {\n type $Typed,\n type AtUriString,\n type DidString,\n type NsidString,\n type UriString,\n getBlobCidString,\n isAtUriString,\n isDidString,\n} from '@atproto/lex'\nimport { AtUri, normalizeDatetimeAlways } from '@atproto/syntax'\nimport type { ExternalRecord } from '../hydration/external.js'\nimport type { HydrationState } from '../hydration/hydrator.js'\nimport { app, com, place, site, social } from '../lexicons/index.js'\nimport { estimateReadingTimeMinutes } from '../util/external/standard-site.js'\nimport { uriToDid } from '../util/uris.js'\nimport type { Views } from './index.js'\nimport type { ExternalRecordView, Label, ProfileViewBasic } from './types.js'\n\nexport const LIVESTREAM_HEARTBEAT_WINDOW_MS = 2 * MINUTE\n\n/** Build a modality view from validated, available records in hydration state. */\nexport function externalRecordView(\n views: Views,\n uri: AtUriString,\n state: HydrationState,\n now: number,\n): $Typed<ExternalRecordView> | undefined {\n const info = availableRecord(views, uri, state)\n if (!info) return\n const { record } = info\n\n if (site.standard.document.$matches(record)) {\n let publisher:\n $Typed<app.bsky.embed.external.ViewArticlePublication> | undefined\n let base = record.site\n const refs = [uri]\n if (isAtUriString(record.site)) {\n publisher = publicationView(views, record.site, state)\n if (!publisher) return\n base = publisher.uri\n refs.push(record.site)\n }\n const url = httpUri(\n record.path\n ? `${base.replace(/\\/$/, '')}/${record.path.replace(/^\\//, '')}`\n : base,\n )\n if (!url || !record.title) return\n return app.bsky.embed.external.viewArticle.$build({\n ...commonFields(views, refs, state),\n uri: url,\n title: record.title,\n description: record.description ?? '',\n createdAt: record.publishedAt,\n updatedAt: record.updatedAt,\n image: record.coverImage\n ? views.imgUriBuilder.getPresetUri(\n 'feed_thumbnail',\n uriToDid(uri),\n getBlobCidString(record.coverImage),\n )\n : undefined,\n publisher,\n readingTime: record.textContent\n ? estimateReadingTimeMinutes(record.textContent)\n : undefined,\n likeCount: backlinkCount(uri, site.standard.graph.recommend.$type, state),\n likers: backlinkProfiles(views, uri, state, 'recommend'),\n })\n }\n\n if (site.standard.publication.$matches(record)) {\n return publicationView(views, uri, state)\n }\n\n if (social.grain.gallery.$matches(record)) {\n const links = (state.externalRecordBacklinks?.get(uri) ?? []).flatMap(\n (linkUri) => {\n const link = availableRecord(views, linkUri, state)?.record\n if (\n !social.grain.gallery.item.$matches(link) ||\n link.gallery !== uri ||\n uriToDid(linkUri) !== uriToDid(uri)\n ) {\n return []\n }\n return [{ uri: linkUri, record: link }]\n },\n )\n links.sort(\n (a, b) =>\n (a.record.position ?? 0) - (b.record.position ?? 0) ||\n compareStrings(a.uri, b.uri),\n )\n\n const refs = [uri]\n const seenPhotos = new Set<AtUriString>()\n const items: $Typed<app.bsky.embed.external.ViewGalleryImage>[] = []\n for (const link of links) {\n const photoUri = link.record.item\n if (seenPhotos.has(photoUri)) continue\n const photo = availableRecord(views, photoUri, state)?.record\n if (!social.grain.photo.$matches(photo)) continue\n seenPhotos.add(photoUri)\n refs.push(link.uri, photoUri)\n items.push(\n app.bsky.embed.external.viewGalleryImage.$build({\n thumbnail: views.imgUriBuilder.getPresetUri(\n 'feed_thumbnail',\n uriToDid(photoUri),\n getBlobCidString(photo.photo),\n ),\n fullsize: views.imgUriBuilder.getPresetUri(\n 'feed_fullsize',\n uriToDid(photoUri),\n getBlobCidString(photo.photo),\n ),\n alt: photo.alt,\n aspectRatio: photo.aspectRatio\n ? {\n width: photo.aspectRatio.width,\n height: photo.aspectRatio.height,\n }\n : undefined,\n }),\n )\n }\n return app.bsky.embed.external.viewGallery.$build({\n ...commonFields(views, refs, state),\n uri,\n title: record.title,\n description: record.description ?? '',\n createdAt: record.createdAt,\n items,\n likeCount: backlinkCount(uri, social.grain.favorite.$type, state),\n likers: backlinkProfiles(views, uri, state, 'favorite'),\n })\n }\n\n if (place.stream.livestream.$matches(record)) {\n const lastSeen = record.lastSeenAt ? Date.parse(record.lastSeenAt) : NaN\n // @NOTE The window applies on both sides of now to tolerate clock skew, so\n // a future heartbeat counts as live for at most twice the window.\n const fresh = Math.abs(now - lastSeen) < LIVESTREAM_HEARTBEAT_WINDOW_MS\n return app.bsky.embed.external.viewLivestream.$build({\n ...commonFields(views, [uri], state),\n uri: httpUri(record.canonicalUrl) ?? httpUri(record.url) ?? uri,\n title: record.title,\n description: '',\n createdAt: record.createdAt,\n image: record.thumb\n ? views.imgUriBuilder.getPresetUri(\n 'feed_thumbnail',\n uriToDid(uri),\n getBlobCidString(record.thumb),\n )\n : undefined,\n active: !record.endedAt && fresh,\n startedAt: record.createdAt,\n endedAt: record.endedAt,\n })\n }\n}\n\nfunction publicationView(\n views: Views,\n uri: AtUriString,\n state: HydrationState,\n): $Typed<app.bsky.embed.external.ViewArticlePublication> | undefined {\n const record = availableRecord(views, uri, state)?.record\n if (!site.standard.publication.$matches(record)) return\n const url = httpUri(record.url)\n if (!url || !record.name) return\n return app.bsky.embed.external.viewArticlePublication.$build({\n ...commonFields(views, [uri], state),\n uri: url,\n title: record.name,\n description: record.description ?? '',\n logo: record.icon\n ? views.imgUriBuilder.getPresetUri(\n 'avatar',\n uriToDid(uri),\n getBlobCidString(record.icon),\n )\n : undefined,\n theme: record.basicTheme\n ? app.bsky.embed.external.viewArticlePublicationTheme.$build({\n background: hexColor(record.basicTheme.background),\n foreground: hexColor(record.basicTheme.foreground),\n accent: hexColor(record.basicTheme.accent),\n accentForeground: hexColor(record.basicTheme.accentForeground),\n })\n : undefined,\n subscriptionCount: backlinkCount(\n uri,\n site.standard.graph.subscription.$type,\n state,\n ),\n subscribers: backlinkProfiles(views, uri, state, 'subscription'),\n })\n}\n\nfunction availableRecord(\n views: Views,\n uri: AtUriString,\n state: HydrationState,\n): ExternalRecord | undefined {\n const info = state.externalRecords?.get(uri)\n if (!info) return\n const parsed = new AtUri(uri)\n if (!isDidString(parsed.host) || parsed.collection !== info.record.$type)\n return\n // @NOTE Actor hydration nulls unavailable accounts (missing and tombstoned\n // ones even with includeTakedowns). Absent entries just weren't hydrated.\n if (state.actors?.get(parsed.host) === null) return\n if (views.viewerBlockExists(parsed.host, state)) return\n if (!views.viewerSeesNeedsReview({ uri }, state)) return\n if (\n !state.ctx?.includeTakedowns &&\n (info.takedownRef ||\n state.labels?.get(uri)?.isTakendown ||\n views.actorIsNoHosted(parsed.host, state))\n ) {\n return\n }\n return info\n}\n\nfunction commonFields(\n views: Views,\n uris: AtUriString[],\n state: HydrationState,\n) {\n const refs = [...new Set(uris)]\n const dids = [...new Set(refs.map(uriToDid))]\n return {\n associatedRefs: refs.map((uri) =>\n com.atproto.repo.strongRef.$build({\n uri,\n cid: state.externalRecords!.get(uri)!.cid,\n }),\n ),\n associatedProfiles: dids.flatMap((did) => {\n const profile = views.profileBasic(did, state)\n return profile ? [profile] : []\n }),\n labels: refs.flatMap((uri) =>\n recordLabels(uri, state.externalRecords!.get(uri)!, state),\n ),\n }\n}\n\nfunction recordLabels(\n uri: AtUriString,\n info: ExternalRecord,\n state: HydrationState,\n): Label[] {\n const labels = state.labels?.getBySubject(uri) ?? []\n const selfLabels = info.record.labels\n if (!com.atproto.label.defs.selfLabels.$matches(selfLabels)) return labels\n const createdAt = info.record.createdAt ?? info.record.publishedAt\n const cts =\n typeof createdAt === 'string'\n ? normalizeDatetimeAlways(createdAt)\n : '1970-01-01T00:00:00.000Z'\n return [\n ...labels,\n ...selfLabels.values.map(({ val }) => ({\n src: uriToDid(uri),\n uri,\n cid: info.cid,\n val,\n cts,\n })),\n ]\n}\n\nfunction backlinkCount(\n uri: AtUriString,\n collection: NsidString,\n state: HydrationState,\n): number | undefined {\n const counts = state.externalRecordBacklinkCounts?.get(uri)\n if (!counts) return\n const count = counts[collection] ?? 0\n return Number.isSafeInteger(count) && count >= 0 ? count : undefined\n}\n\nfunction backlinkProfiles(\n views: Views,\n uri: AtUriString,\n state: HydrationState,\n kind: 'recommend' | 'subscription' | 'favorite',\n): ProfileViewBasic[] | undefined {\n const backlinks = state.externalRecordBacklinks?.get(uri)\n if (!backlinks) return\n const dids = new Set<DidString>()\n for (const backlink of backlinks) {\n const record = availableRecord(views, backlink, state)?.record\n const matches =\n kind === 'recommend'\n ? site.standard.graph.recommend.$matches(record) &&\n record.document === uri\n : kind === 'subscription'\n ? site.standard.graph.subscription.$matches(record) &&\n record.publication === uri\n : social.grain.favorite.$matches(record) && record.subject === uri\n if (matches) dids.add(uriToDid(backlink))\n }\n const profiles: ProfileViewBasic[] = []\n for (const did of [...dids].sort(compareStrings)) {\n const profile = views.profileBasic(did, state)\n if (profile) profiles.push(profile)\n if (profiles.length === 3) break\n }\n return profiles\n}\n\nfunction hexColor(\n color: site.standard.theme.basic.Main['background'],\n): string | undefined {\n if (site.standard.theme.color.rgb.$matches(color)) {\n return `#${[color.r, color.g, color.b].map(hexByte).join('')}`\n }\n}\n\nfunction hexByte(value: number): string {\n return value.toString(16).padStart(2, '0')\n}\n\nfunction httpUri(value: string | undefined): UriString | undefined {\n if (!value) return\n try {\n const url = new URL(value)\n if (url.protocol === 'http:' || url.protocol === 'https:')\n return url.href as UriString\n } catch {\n return\n }\n}\n\nfunction compareStrings(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0\n}\n"]}
@@ -9,8 +9,11 @@ import type { Label } from '../hydration/label.js';
9
9
  import type { ImageUriBuilder } from '../image/uri.js';
10
10
  import { app } from '../lexicons/index.js';
11
11
  import type { Notification } from '../proto/bsky_pb.js';
12
- import { type ActivitySubscription, type BlockedPost, type BookmarkView, type Embed, type EmbedView, type ExternalEmbed, type ExternalEmbedSourceView, type ExternalEmbedView, type ExternalRecordView, type FeedViewPost, type FollowRecord, type GalleryEmbed, type GalleryEmbedView, type GeneratorView, type GetPostThreadV2QueryParams, type ImagesEmbed, type ImagesEmbedView, type KnownFollowers, type KnownLikers, type LabelerRecord, type LabelerView, type LabelerViewDetailed, type LikeRecord, type ListItemView, type ListView, type ListViewBasic, type MaybePostView, type NotFoundPost, type NotificationRecordDeleted, type NotificationView, type PostEmbedView, type PostRecord, type PostView, type ProfileAssociatedActivitySubscription, type ProfileAssociatedChat, type ProfileRecord, type ProfileView, type ProfileViewBasic, type ProfileViewDetailed, type ProfileViewer, type ReasonPin, type ReasonRepost, type RecordEmbed, type RecordEmbedView, type RecordWithMedia, type RecordWithMediaView, type ReplyRef, type RepostRecord, type StarterPackView, type StarterPackViewBasic, type StatusView, type ThreadItem, type ThreadOtherItem, type ThreadViewPost, type ThreadgateView, type VerificationRecord, type VerificationState, type VideoEmbed, type VideoEmbedView } from './types.js';
12
+ import { type ActivitySubscription, type BlockedPost, type BookmarkView, type Embed, type EmbedView, type ExternalEmbed, type ExternalEmbedSourceView, type ExternalEmbedView, type ExternalRecordView, type FeedViewPost, type FollowRecord, type GalleryEmbed, type GalleryEmbedView, type GeneratorView, type GetPostThreadV2QueryParams, type ImagesEmbed, type ImagesEmbedView, type KnownFollowers, type KnownLikers, type LabelerRecord, type LabelerView, type LabelerViewDetailed, type LikeRecord, type ListItemView, type ListView, type ListViewBasic, type MaybePostView, type NotFoundPost, type NotificationRecordDeleted, type NotificationView, type PostEmbedView, type PostRecord, type PostView, type ProfileAssociatedActivitySubscription, type ProfileAssociatedChat, type ProfileLinkView, type ProfileRecord, type ProfileView, type ProfileViewBasic, type ProfileViewDetailed, type ProfileViewer, type ReasonPin, type ReasonRepost, type RecordEmbed, type RecordEmbedView, type RecordWithMedia, type RecordWithMediaView, type ReplyRef, type RepostRecord, type StarterPackView, type StarterPackViewBasic, type StatusView, type ThreadItem, type ThreadOtherItem, type ThreadViewPost, type ThreadgateView, type VerificationRecord, type VerificationState, type VideoEmbed, type VideoEmbedView } from './types.js';
13
13
  import { type VideoUriBuilder } from './util.js';
14
+ type ProfileViewOptions = {
15
+ includeKnownFollowers?: boolean;
16
+ };
14
17
  export declare class Views {
15
18
  private opts;
16
19
  imgUriBuilder: ImageUriBuilder;
@@ -46,12 +49,12 @@ export declare class Views {
46
49
  uri?: AtUriString;
47
50
  }, state: HydrationState): boolean;
48
51
  replyIsHiddenByThreadgate(replyUri: AtUriString, rootPostUri: AtUriString, state: HydrationState): boolean;
49
- profileDetailed(did: DidString, state: HydrationState): Un$Typed<ProfileViewDetailed> | undefined;
50
- profile(did: DidString, state: HydrationState): Un$Typed<ProfileView> | undefined;
51
- profileBasic(did: DidString, state: HydrationState): Un$Typed<ProfileViewBasic> | undefined;
52
+ profileDetailed(did: DidString, state: HydrationState, opts?: ProfileViewOptions): Un$Typed<ProfileViewDetailed> | undefined;
53
+ profileLinks(did: DidString, state: HydrationState): ProfileLinkView[] | undefined;
54
+ profile(did: DidString, state: HydrationState, opts?: ProfileViewOptions): Un$Typed<ProfileView> | undefined;
55
+ profileBasic(did: DidString, state: HydrationState, opts?: ProfileViewOptions): Un$Typed<ProfileViewBasic> | undefined;
52
56
  profileAssociatedChat(actor: Actor): ProfileAssociatedChat | undefined;
53
57
  profileAssociatedActivitySubscription(actor: Actor): ProfileAssociatedActivitySubscription;
54
- profileKnownFollowers(did: DidString, state: HydrationState): ProfileView | undefined;
55
58
  profileViewer(did: DidString, state: HydrationState): ProfileViewer | undefined;
56
59
  profileViewerActivitySubscription(profileViewer: ProfileViewerState, did: DidString, state: HydrationState): ActivitySubscription | undefined;
57
60
  profileWebsite(did: DidString, state: HydrationState): UriString | undefined;
@@ -172,7 +175,7 @@ export declare class Views {
172
175
  /**
173
176
  * Compose-path entry point: caller already knows which document and
174
177
  * publication backed the request and has them in hand (e.g. from
175
- * iterating the SS hydration maps directly). Skips the by-ref lookup
178
+ * `getStandardSiteRecordsForUri`). Skips the by-ref lookup
176
179
  * that `externalEmbedFromStandardSite` does. Used by
177
180
  * `getEmbedExternalView`.
178
181
  *
@@ -213,4 +216,5 @@ export declare class Views {
213
216
  indexedAt: Date;
214
217
  }): Date;
215
218
  }
219
+ export {};
216
220
  //# sourceMappingURL=index.d.ts.map