@agent-native/core 0.201.1 → 0.202.0-nightly-20261006033736

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 (98) hide show
  1. package/dist/automation/index.d.ts +1 -1
  2. package/dist/client/analytics/index.d.ts +1 -1
  3. package/dist/client/analytics/index.js +1 -1
  4. package/dist/client/analytics.d.ts +23 -3
  5. package/dist/client/analytics.js +189 -46
  6. package/dist/client/client-status-requests.d.ts +4 -1
  7. package/dist/client/client-status-requests.js +21 -5
  8. package/dist/client/embed-auth.d.ts +6 -0
  9. package/dist/client/embed-auth.js +13 -0
  10. package/dist/client/session-replay.js +48 -22
  11. package/dist/client/sharing/index.d.ts +2 -0
  12. package/dist/client/sharing/index.js +2 -0
  13. package/dist/client/sharing/useAccessRequestReview.d.ts +80 -0
  14. package/dist/client/sharing/useAccessRequestReview.js +100 -0
  15. package/dist/client/sharing/useResourceAccessGate.d.ts +57 -0
  16. package/dist/client/sharing/useResourceAccessGate.js +76 -0
  17. package/dist/client/use-agent-engine-configured.d.ts +2 -0
  18. package/dist/client/use-agent-engine-configured.js +4 -1
  19. package/dist/email-catalog/registry.d.ts +1 -1
  20. package/dist/email-catalog/registry.js +1 -1
  21. package/dist/email-catalog/system-emails.d.ts +2 -2
  22. package/dist/email-catalog/system-emails.js +47 -2
  23. package/dist/email-catalog/templates.d.ts +5 -1
  24. package/dist/email-catalog/templates.js +11 -1
  25. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  26. package/dist/framework-tools.js +6 -0
  27. package/dist/identity/rekey.js +9 -0
  28. package/dist/jobs/run-history.d.ts +27 -8
  29. package/dist/jobs/run-history.js +17 -10
  30. package/dist/jobs/scheduler.js +39 -10
  31. package/dist/mcp/build-server.d.ts +1 -0
  32. package/dist/mcp/build-server.js +30 -7
  33. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  34. package/dist/notifications/routes.d.ts +2 -2
  35. package/dist/observability/routes.d.ts +7 -7
  36. package/dist/org/index.d.ts +1 -1
  37. package/dist/progress/routes.d.ts +1 -1
  38. package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
  39. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  40. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  41. package/dist/review/suggestions/actions.js +14 -35
  42. package/dist/review/suggestions/store.d.ts +0 -1
  43. package/dist/review/suggestions/store.js +0 -7
  44. package/dist/secrets/routes.d.ts +11 -11
  45. package/dist/server/action-discovery.js +24 -0
  46. package/dist/server/agent-access.js +7 -3
  47. package/dist/server/agent-chat/mcp-options.d.ts +1 -0
  48. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  49. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  50. package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
  51. package/dist/server/attribution.d.ts +33 -1
  52. package/dist/server/attribution.js +93 -21
  53. package/dist/server/auth.js +35 -11
  54. package/dist/server/email-templates.d.ts +34 -0
  55. package/dist/server/email-templates.js +40 -0
  56. package/dist/server/magic-link-attribution.js +2 -1
  57. package/dist/server/onboarding-html.js +35 -67
  58. package/dist/server/release-schema.js +4 -0
  59. package/dist/server/short-lived-token.d.ts +7 -0
  60. package/dist/server/short-lived-token.js +59 -0
  61. package/dist/shared/analytics-anonymous-id.d.ts +1 -0
  62. package/dist/shared/analytics-anonymous-id.js +1 -1
  63. package/dist/shared/attribution-source.d.ts +30 -0
  64. package/dist/shared/attribution-source.js +65 -0
  65. package/dist/sharing/access-request-store.d.ts +96 -0
  66. package/dist/sharing/access-request-store.js +213 -0
  67. package/dist/sharing/access-requests.d.ts +113 -0
  68. package/dist/sharing/access-requests.js +490 -0
  69. package/dist/sharing/access.d.ts +36 -2
  70. package/dist/sharing/access.js +74 -12
  71. package/dist/sharing/actions/approve-resource-access-request.d.ts +10 -0
  72. package/dist/sharing/actions/approve-resource-access-request.js +15 -0
  73. package/dist/sharing/actions/decline-resource-access-request.d.ts +7 -0
  74. package/dist/sharing/actions/decline-resource-access-request.js +14 -0
  75. package/dist/sharing/actions/get-resource-access-request.d.ts +4 -0
  76. package/dist/sharing/actions/get-resource-access-request.js +13 -0
  77. package/dist/sharing/actions/get-resource-access-status.d.ts +5 -0
  78. package/dist/sharing/actions/get-resource-access-status.js +19 -0
  79. package/dist/sharing/actions/list-resource-access-requests.d.ts +5 -0
  80. package/dist/sharing/actions/list-resource-access-requests.js +16 -0
  81. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  82. package/dist/sharing/actions/request-resource-access.d.ts +6 -0
  83. package/dist/sharing/actions/request-resource-access.js +17 -0
  84. package/dist/sharing/actions/share-resource.d.ts +1 -1
  85. package/dist/sharing/actions/share-resource.js +18 -144
  86. package/dist/sharing/grant.d.ts +49 -0
  87. package/dist/sharing/grant.js +195 -0
  88. package/dist/sharing/index.d.ts +2 -1
  89. package/dist/sharing/index.js +2 -1
  90. package/dist/sharing/registry.d.ts +36 -0
  91. package/dist/templates/chat/server/plugins/auth.ts +2 -2
  92. package/dist/templates/default/app/routes/access-requests.$requestId.tsx +7 -0
  93. package/dist/templates/default/server/plugins/auth.ts +2 -2
  94. package/dist/vite/action-types-plugin.js +24 -0
  95. package/package.json +11 -3
  96. package/src/templates/chat/server/plugins/auth.ts +2 -2
  97. package/src/templates/default/app/routes/access-requests.$requestId.tsx +7 -0
  98. package/src/templates/default/server/plugins/auth.ts +2 -2
@@ -1812,6 +1812,34 @@ export function getOnboardingHtml(opts = {}) {
1812
1812
  text-align: center;
1813
1813
  }
1814
1814
  .auth-fallback p { opacity: 0.65; font-size: 0.8125rem; }
1815
+ .auth-page {
1816
+ --auth-wave-bg: #0a0a0a; /* guard:allow-raw-color - standalone auth canvas base */
1817
+ --auth-wave-fg: #aeadac; /* guard:allow-raw-color - shared wave foreground */
1818
+ --b-bg-page: var(--auth-wave-bg);
1819
+ --b-hero-shader-fg: var(--auth-wave-fg);
1820
+ --b-hero-shader-brightness: 3;
1821
+ --b-hero-shader-opacity: 0.3;
1822
+ position: relative;
1823
+ isolation: isolate;
1824
+ display: flex;
1825
+ flex-direction: column;
1826
+ align-items: center;
1827
+ justify-content: center;
1828
+ width: 100%;
1829
+ min-height: 100vh;
1830
+ min-height: 100svh;
1831
+ }
1832
+ .auth-page[data-auth-marketing="true"] {
1833
+ align-items: stretch;
1834
+ justify-content: flex-start;
1835
+ }
1836
+ .auth-page-content { position: relative; z-index: 1; width: 100%; }
1837
+ .auth-wave-background {
1838
+ position: fixed;
1839
+ inset: 0;
1840
+ z-index: 0;
1841
+ pointer-events: none;
1842
+ }
1815
1843
  .auth-centered {
1816
1844
  display: flex;
1817
1845
  justify-content: center;
@@ -1859,13 +1887,6 @@ export function getOnboardingHtml(opts = {}) {
1859
1887
  overflow: hidden;
1860
1888
  padding: 3rem 3.5rem;
1861
1889
  }
1862
- .auth-marketing-visual > [data-agent-native-marketing-background] {
1863
- position: absolute;
1864
- inset: 0;
1865
- width: 100%;
1866
- height: 100%;
1867
- transform: none;
1868
- }
1869
1890
  .auth-marketing-visual .marketing-content {
1870
1891
  position: relative;
1871
1892
  z-index: 1;
@@ -1890,7 +1911,6 @@ export function getOnboardingHtml(opts = {}) {
1890
1911
  }
1891
1912
  .auth-marketing-home .form-panel > .card { margin-block: auto; }
1892
1913
  @media not all and (min-width: 901px) {
1893
- .auth-marketing-home .auth-marketing-screenshot-wrap { display: none; }
1894
1914
  body.has-marketing {
1895
1915
  align-items: flex-start;
1896
1916
  justify-content: flex-start;
@@ -1935,7 +1955,7 @@ export function getOnboardingHtml(opts = {}) {
1935
1955
  font-synthesis: none;
1936
1956
  }
1937
1957
  .auth-marketing-home {
1938
- background: var(--auth-marketing-right-bg);
1958
+ background: transparent;
1939
1959
  color: var(--auth-marketing-foreground);
1940
1960
  }
1941
1961
  .auth-marketing-home .auth-marketing-top-right {
@@ -1957,43 +1977,12 @@ export function getOnboardingHtml(opts = {}) {
1957
1977
  border: 0;
1958
1978
  }
1959
1979
  .auth-marketing-home .marketing-panel {
1960
- background: var(--auth-marketing-left-bg);
1980
+ background: transparent;
1961
1981
  }
1962
1982
  .auth-marketing-home .auth-marketing-visual {
1963
1983
  min-height: 100vh;
1964
1984
  padding: 4.5rem 5rem 4rem;
1965
- background: var(--auth-marketing-left-bg);
1966
- }
1967
- .auth-marketing-home .auth-marketing-screenshot-wrap {
1968
- position: fixed;
1969
- inset: 0;
1970
- z-index: 0;
1971
- display: block;
1972
- width: 100%;
1973
- height: 100%;
1974
- max-width: none;
1975
- max-height: none;
1976
- margin: 0;
1977
- border-radius: 0;
1978
- box-shadow: none;
1979
- }
1980
- .auth-marketing-home .auth-marketing-screenshot {
1981
- display: block;
1982
- width: 100%;
1983
- height: 100%;
1984
- max-width: none;
1985
- max-height: none;
1986
- filter: none;
1987
- background:
1988
- radial-gradient(ellipse 82% 72% at 80% 64%, color-mix(in srgb, var(--auth-marketing-foreground) 16%, transparent), transparent 72%),
1989
- radial-gradient(ellipse 48% 36% at 24% 26%, color-mix(in srgb, var(--auth-marketing-foreground) 8%, transparent), transparent 82%);
1990
- }
1991
- .auth-marketing-home [data-agent-native-marketing-background] {
1992
- position: fixed;
1993
- inset: 0;
1994
- width: 100%;
1995
- height: 100%;
1996
- transform: translateY(-5vh);
1985
+ background: transparent;
1997
1986
  }
1998
1987
  .auth-marketing-home .auth-marketing-visual .marketing-content {
1999
1988
  min-height: calc(100vh - 8.5rem);
@@ -2070,10 +2059,11 @@ export function getOnboardingHtml(opts = {}) {
2070
2059
  }
2071
2060
  .auth-marketing-home .form-panel {
2072
2061
  padding: 0 5rem;
2073
- background: var(--auth-marketing-right-bg);
2062
+ background: transparent;
2074
2063
  border-inline-start: 1px solid var(--auth-marketing-border);
2075
2064
  position: relative;
2076
2065
  z-index: 1;
2066
+ backdrop-filter: blur(10px);
2077
2067
  }
2078
2068
  .auth-marketing-home .form-panel > .card {
2079
2069
  width: min(27.5rem, 100%);
@@ -2124,17 +2114,6 @@ export function getOnboardingHtml(opts = {}) {
2124
2114
  text-decoration: underline;
2125
2115
  text-underline-offset: 0.125rem;
2126
2116
  }
2127
- .auth-marketing-home .auth-marketing-signup-wave {
2128
- height: clamp(10rem, 24vh, 15rem);
2129
- margin-top: 1.25rem;
2130
- opacity: 0.6;
2131
- overflow: hidden;
2132
- pointer-events: none;
2133
- }
2134
- .auth-marketing-home .auth-marketing-signup-wave-canvas {
2135
- width: 100%;
2136
- height: 100%;
2137
- }
2138
2117
  body.has-marketing .locale-picker {
2139
2118
  top: auto;
2140
2119
  bottom: max(1.25rem, env(safe-area-inset-bottom));
@@ -2142,20 +2121,12 @@ export function getOnboardingHtml(opts = {}) {
2142
2121
  }
2143
2122
  @media (prefers-color-scheme: light) {
2144
2123
  body.has-marketing {
2145
- --b-hero-ocean-opacity: 0.3;
2146
- --auth-marketing-left-bg: Canvas;
2147
- --auth-marketing-right-bg: Canvas;
2148
- --auth-marketing-foreground: CanvasText;
2149
- --auth-marketing-muted: GrayText;
2150
- --auth-marketing-subtle: GrayText;
2151
- --auth-marketing-border: color-mix(in srgb, CanvasText 18%, transparent);
2152
- --auth-marketing-badge-bg: color-mix(in srgb, CanvasText 7%, Canvas);
2153
- color-scheme: light;
2124
+ --auth-marketing-border: color-mix(in srgb, var(--auth-marketing-foreground) 18%, transparent);
2154
2125
  }
2155
2126
  .auth-marketing-home .auth-marketing-visual,
2156
2127
  .auth-marketing-home .marketing-panel,
2157
2128
  .auth-marketing-home .form-panel {
2158
- background: Canvas;
2129
+ background: transparent;
2159
2130
  }
2160
2131
  .auth-marketing-home .card input {
2161
2132
  color: var(--auth-marketing-foreground);
@@ -2174,9 +2145,6 @@ export function getOnboardingHtml(opts = {}) {
2174
2145
  .auth-marketing-home .card button[type="submit"] {
2175
2146
  border: 1px solid var(--auth-marketing-border);
2176
2147
  }
2177
- .auth-marketing-home .app-name img.brand-mark {
2178
- filter: grayscale(1) brightness(0);
2179
- }
2180
2148
  }
2181
2149
  @media not all and (min-width: 901px) {
2182
2150
  .auth-marketing-home {
@@ -225,6 +225,10 @@ const FRAMEWORK_SCHEMA_ENSURES = [
225
225
  "RemoteRunEvents",
226
226
  () => import("../integrations/remote-run-events-store.js").then((m) => m.ensureTable()),
227
227
  ],
228
+ [
229
+ "ResourceAccessRequests",
230
+ () => import("../sharing/access-request-store.js").then((m) => m.ensureTable()),
231
+ ],
228
232
  [
229
233
  "ResourceChanges",
230
234
  () => import("../resource-changes/store.js").then((m) => m.ensureResourceChangeTables()),
@@ -31,6 +31,13 @@ export declare function signShortLivedToken(claims: ShortLivedTokenClaims): stri
31
31
  * not surface the reason to viewers (it's useful for server-side logs only).
32
32
  */
33
33
  export declare function verifyShortLivedToken(token: string, expectedResourceId: string): VerifyResult;
34
+ export declare function signCompactShortLivedToken(claims: ShortLivedTokenClaims): string;
35
+ /**
36
+ * Verify a token produced by {@link signCompactShortLivedToken}. A token for a
37
+ * different resource, and any legacy token, fails as `bad_signature` — the id
38
+ * is not in the payload, so a mismatch is indistinguishable from tampering.
39
+ */
40
+ export declare function verifyCompactShortLivedToken(token: string, expectedResourceId: string): VerifyResult;
34
41
  /** Payload `typ` discriminator for realtime subscribe tokens. */
35
42
  export declare const REALTIME_SUBSCRIBE_TOKEN_TYPE = "rt-subscribe";
36
43
  export interface RealtimeSubscribeClaims {
@@ -1,6 +1,7 @@
1
1
  import crypto from "node:crypto";
2
2
  import { getWorkspaceA2ADerivedSecret } from "./derived-secret.js";
3
3
  const DEFAULT_TTL_SECONDS = 600;
4
+ const MAX_COMPACT_AGENT_LABEL_ENCODED_BYTES = 16;
4
5
  let _devSigningKey;
5
6
  function getSigningKey() {
6
7
  const secret = process.env.OAUTH_STATE_SECRET ||
@@ -94,6 +95,64 @@ export function verifyShortLivedToken(token, expectedResourceId) {
94
95
  agentLabel: claims.agentLabel,
95
96
  };
96
97
  }
98
+ function compactSignature(resourceId, payloadStr) {
99
+ return hmacB64(`agent-access\n${resourceId}\n${payloadStr}`, getSigningKey());
100
+ }
101
+ export function signCompactShortLivedToken(claims) {
102
+ const ttl = claims.ttlSeconds ?? DEFAULT_TTL_SECONDS;
103
+ const payload = {
104
+ e: Math.floor(Date.now() / 1000) + ttl,
105
+ };
106
+ if (claims.viewerEmail)
107
+ payload.v = claims.viewerEmail;
108
+ if (claims.agentLabel) {
109
+ let label = "";
110
+ let encodedBytes = 0;
111
+ for (const character of claims.agentLabel) {
112
+ const characterBytes = Buffer.byteLength(JSON.stringify(character)) - 2;
113
+ if (encodedBytes + characterBytes > MAX_COMPACT_AGENT_LABEL_ENCODED_BYTES)
114
+ break;
115
+ label += character;
116
+ encodedBytes += characterBytes;
117
+ }
118
+ if (label)
119
+ payload.l = label;
120
+ }
121
+ const payloadStr = base64UrlEncode(JSON.stringify(payload));
122
+ return `${payloadStr}.${compactSignature(claims.resourceId, payloadStr)}`;
123
+ }
124
+ /**
125
+ * Verify a token produced by {@link signCompactShortLivedToken}. A token for a
126
+ * different resource, and any legacy token, fails as `bad_signature` — the id
127
+ * is not in the payload, so a mismatch is indistinguishable from tampering.
128
+ */
129
+ export function verifyCompactShortLivedToken(token, expectedResourceId) {
130
+ if (typeof token !== "string" || !token.includes(".")) {
131
+ return { ok: false, reason: "malformed" };
132
+ }
133
+ const [payloadStr, sig] = token.split(".", 2);
134
+ if (!payloadStr || !sig)
135
+ return { ok: false, reason: "malformed" };
136
+ if (!timingSafeEqualB64(sig, compactSignature(expectedResourceId, payloadStr))) {
137
+ return { ok: false, reason: "bad_signature" };
138
+ }
139
+ let claims;
140
+ try {
141
+ claims = JSON.parse(base64UrlDecode(payloadStr).toString("utf8"));
142
+ }
143
+ catch {
144
+ return { ok: false, reason: "bad_payload" };
145
+ }
146
+ if (typeof claims.e !== "number")
147
+ return { ok: false, reason: "bad_payload" };
148
+ if ((claims.v !== undefined && typeof claims.v !== "string") ||
149
+ (claims.l !== undefined && typeof claims.l !== "string")) {
150
+ return { ok: false, reason: "bad_payload" };
151
+ }
152
+ if (claims.e * 1000 < Date.now())
153
+ return { ok: false, reason: "expired" };
154
+ return { ok: true, viewerEmail: claims.v, agentLabel: claims.l };
155
+ }
97
156
  // ── Realtime subscribe tokens ────────────────────────────────────────────────
98
157
  //
99
158
  // An identity-bearing extension of the same HMAC discipline, used by the hosted
@@ -1,4 +1,5 @@
1
1
  export declare const ANALYTICS_ANONYMOUS_ID_COOKIE_NAME = "an_aid";
2
2
  export declare const ANALYTICS_ANONYMOUS_ID_COOKIE_MAX_AGE_SECONDS: number;
3
+ export declare const ANALYTICS_ANONYMOUS_ID_MAX_LENGTH = 128;
3
4
  export declare function normalizeAnalyticsAnonymousId(value: unknown): string | undefined;
4
5
  export declare function serializeAnalyticsAnonymousIdCookie(value: unknown): string | undefined;
@@ -1,6 +1,6 @@
1
1
  export const ANALYTICS_ANONYMOUS_ID_COOKIE_NAME = "an_aid";
2
2
  export const ANALYTICS_ANONYMOUS_ID_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 30;
3
- const ANALYTICS_ANONYMOUS_ID_MAX_LENGTH = 128;
3
+ export const ANALYTICS_ANONYMOUS_ID_MAX_LENGTH = 128;
4
4
  const ANALYTICS_ANONYMOUS_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
5
5
  export function normalizeAnalyticsAnonymousId(value) {
6
6
  if (typeof value !== "string")
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Whether a visit says where the person came from. The browser uses this to
3
+ * decide which visits to keep as first and last touch, and the server uses the
4
+ * same share-link rules to derive `referral_source`, so the two can't drift.
5
+ */
6
+ export interface AttributionTouch {
7
+ ref?: string;
8
+ via?: string;
9
+ utm_source?: string;
10
+ utm_medium?: string;
11
+ utm_campaign?: string;
12
+ utm_content?: string;
13
+ utm_term?: string;
14
+ gclid?: string;
15
+ msclkid?: string;
16
+ vector_source?: string;
17
+ landing_path?: string;
18
+ landing_referrer?: string;
19
+ site_referrer?: string;
20
+ }
21
+ export declare function shareLandingSource(path: string | undefined): "clip_share" | "plan_share" | undefined;
22
+ export declare function isFirstPartyHost(host: string | undefined): boolean;
23
+ /**
24
+ * Whether a referrer host says where someone came from. Moving between our own
25
+ * apps and the marketing site, a local dev server, or returning from Google
26
+ * sign-in is navigation, not a source. The browser and the server both use
27
+ * this, so capture and `referral_source` agree.
28
+ */
29
+ export declare function isSourceReferrerHost(host: string | undefined): boolean;
30
+ export declare function hasAttributionSource(touch: AttributionTouch | null | undefined): boolean;
@@ -0,0 +1,65 @@
1
+ const TAG_FIELDS = [
2
+ "ref",
3
+ "via",
4
+ "utm_source",
5
+ "utm_medium",
6
+ "utm_campaign",
7
+ "utm_content",
8
+ "utm_term",
9
+ "gclid",
10
+ "msclkid",
11
+ "vector_source",
12
+ ];
13
+ const PLAN_SHARE_PATH_SEGMENTS = [
14
+ "/p/",
15
+ "/plan/",
16
+ "/plans/",
17
+ "/recaps/",
18
+ "/share-plan/",
19
+ ];
20
+ export function shareLandingSource(path) {
21
+ if (!path)
22
+ return undefined;
23
+ if (path.startsWith("/share/"))
24
+ return "clip_share";
25
+ if (PLAN_SHARE_PATH_SEGMENTS.some((segment) => path.includes(segment))) {
26
+ return "plan_share";
27
+ }
28
+ return undefined;
29
+ }
30
+ export function isFirstPartyHost(host) {
31
+ const normalized = host?.trim().toLowerCase().replace(/\.$/, "") ?? "";
32
+ return (normalized === "agent-native.com" ||
33
+ normalized.endsWith(".agent-native.com"));
34
+ }
35
+ // Coming back from Google sign-in sends this referrer. It's a step in signing
36
+ // in, never where someone heard about us.
37
+ const SIGN_IN_HOSTS = new Set(["accounts.google.com"]);
38
+ // A developer's own machine, with or without a port.
39
+ const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]", "0.0.0.0"]);
40
+ /**
41
+ * Whether a referrer host says where someone came from. Moving between our own
42
+ * apps and the marketing site, a local dev server, or returning from Google
43
+ * sign-in is navigation, not a source. The browser and the server both use
44
+ * this, so capture and `referral_source` agree.
45
+ */
46
+ export function isSourceReferrerHost(host) {
47
+ const normalized = host?.trim().toLowerCase().replace(/\.$/, "") ?? "";
48
+ if (!normalized)
49
+ return false;
50
+ const hostname = normalized.startsWith("[")
51
+ ? normalized.slice(0, normalized.indexOf("]") + 1)
52
+ : normalized.split(":")[0];
53
+ return (!isFirstPartyHost(hostname) &&
54
+ !LOCAL_HOSTS.has(hostname) &&
55
+ !SIGN_IN_HOSTS.has(hostname));
56
+ }
57
+ export function hasAttributionSource(touch) {
58
+ if (!touch)
59
+ return false;
60
+ if (TAG_FIELDS.some((field) => !!touch[field]?.trim()))
61
+ return true;
62
+ if (shareLandingSource(touch.landing_path))
63
+ return true;
64
+ return [touch.landing_referrer, touch.site_referrer].some(isSourceReferrerHost);
65
+ }
@@ -0,0 +1,96 @@
1
+ import { type DbExec } from "../db/client.js";
2
+ import type { ShareRole } from "./schema.js";
3
+ /**
4
+ * Requests from signed-in people who hold a link to a resource they can't
5
+ * open. One row per resource and requester: asking again reopens the same row
6
+ * with a new generation, so owners are never notified twice for one ask.
7
+ */
8
+ export type AccessRequestState = "pending" | "approved" | "declined";
9
+ export interface AccessRequestRow {
10
+ id: string;
11
+ resourceType: string;
12
+ resourceId: string;
13
+ requesterEmail: string;
14
+ requesterName: string | null;
15
+ /** The resource owner when the request was made, for per-owner limits. */
16
+ ownerEmail: string;
17
+ note: string | null;
18
+ state: AccessRequestState;
19
+ /** Increments each time a closed request is asked again. */
20
+ generation: number;
21
+ /** When this generation was asked, in epoch ms. */
22
+ requestedAt: number;
23
+ /** When the requester first asked for this resource, in epoch ms. */
24
+ createdAt: number;
25
+ decidedBy: string | null;
26
+ decidedAt: number | null;
27
+ grantedRole: ShareRole | null;
28
+ /** Whether anyone was told about this generation. */
29
+ delivered: boolean;
30
+ }
31
+ export declare function ensureTable(): Promise<void>;
32
+ export declare function getAccessRequest(id: string): Promise<AccessRequestRow | null>;
33
+ export declare function findAccessRequest(resourceType: string, resourceId: string, requesterEmail: string): Promise<AccessRequestRow | null>;
34
+ export declare function listPendingAccessRequests(resourceType: string, resourceId: string, limit: number): Promise<AccessRequestRow[]>;
35
+ /**
36
+ * How many requests were asked since `since` by one requester or against one
37
+ * owner, and when the oldest of them was asked.
38
+ */
39
+ export declare function countRecentAccessRequests(by: {
40
+ requesterEmail: string;
41
+ } | {
42
+ ownerEmail: string;
43
+ }, since: number): Promise<{
44
+ count: number;
45
+ oldestAt: number | null;
46
+ }>;
47
+ export interface OpenAccessRequestInput {
48
+ resourceType: string;
49
+ resourceId: string;
50
+ requesterEmail: string;
51
+ requesterName: string | null;
52
+ ownerEmail: string;
53
+ note: string | null;
54
+ now: number;
55
+ /** A request declined after this time stays closed instead of reopening. */
56
+ declinedCooldownStart: number;
57
+ /**
58
+ * A pending request nobody was told about, asked before this time, is
59
+ * claimed to be sent again; one asked later may still be sending.
60
+ */
61
+ sendWindowStart: number;
62
+ }
63
+ /**
64
+ * Opens a request, or reopens a closed one as a new generation. A pending
65
+ * request whose send was cut off before anyone was told is claimed again in
66
+ * its own generation, so recipients the cut-off send reached get nothing new.
67
+ * Returns `opened: false` with the existing row when a request is already
68
+ * pending, or was declined too recently to ask again, so the caller sends
69
+ * nothing.
70
+ */
71
+ export declare function openAccessRequest(input: OpenAccessRequestInput): Promise<{
72
+ request: AccessRequestRow;
73
+ opened: boolean;
74
+ }>;
75
+ /**
76
+ * Withdraws a request that was just opened but reached nobody, so asking
77
+ * again isn't blocked and doesn't count toward the limits. Only the ask that
78
+ * opened or claimed it at `requestedAt` can: one another ask has claimed
79
+ * since, or already delivered, stays.
80
+ */
81
+ export declare function deleteAccessRequest(id: string, generation: number, requestedAt: number): Promise<void>;
82
+ export declare function recordAccessRequestDelivery(id: string, generation: number, delivery: Record<string, unknown>): Promise<void>;
83
+ /**
84
+ * Closes a pending request. Only a request still pending in the generation
85
+ * the decider saw changes, so a decision on a stale page can't close a newer
86
+ * ask and two people can't both decide one. Pass the transaction a grant runs
87
+ * in so both commit together.
88
+ */
89
+ export declare function decideAccessRequest(input: {
90
+ id: string;
91
+ generation: number;
92
+ state: Exclude<AccessRequestState, "pending">;
93
+ decidedBy: string;
94
+ grantedRole: ShareRole | null;
95
+ now: number;
96
+ }, exec?: DbExec): Promise<AccessRequestRow | null>;