ad2app-lib 1.37.0 → 1.39.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.
@@ -32,6 +32,7 @@ export declare const EVENTS: {
32
32
  readonly PROFILE_COMPLETED: "profile_completed";
33
33
  readonly LOGGED_IN: "logged_in";
34
34
  readonly SOCIAL_ACCOUNT_CONNECTED: "social_account_connected";
35
+ readonly SOCIAL_SIGN_IN_FAILED: "social_sign_in_failed";
35
36
  readonly COMPOSE_STARTED: "compose_started";
36
37
  readonly COMPOSE_STEP_VIEWED: "compose_step_viewed";
37
38
  readonly COMPOSE_BLOCKED: "compose_blocked";
@@ -44,6 +45,9 @@ export declare const EVENTS: {
44
45
  readonly CHECKOUT_STARTED: "checkout_started";
45
46
  readonly SUBSCRIPTION_STARTED: "subscription_started";
46
47
  readonly SUBSCRIPTION_CANCELED: "subscription_canceled";
48
+ readonly BILLING_PORTAL_OPENED: "billing_portal_opened";
49
+ readonly SUBSCRIPTION_REACTIVATED: "subscription_reactivated";
50
+ readonly CHECKOUT_CONFIRMATION_TIMED_OUT: "checkout_confirmation_timed_out";
47
51
  readonly PAYWALL_SHOWN: "paywall_shown";
48
52
  readonly PAYWALL_DISMISSED: "paywall_dismissed";
49
53
  readonly ACCESS_DENIED: "access_denied";
@@ -75,6 +79,12 @@ export declare const EVENTS: {
75
79
  readonly POST_RESCHEDULED: "post_rescheduled";
76
80
  readonly ACCOUNT_DISCONNECTED: "account_disconnected";
77
81
  readonly NOTIFICATION_PREFERENCES_UPDATED: "notification_preferences_updated";
82
+ readonly THEME_CHANGED: "theme_changed";
83
+ readonly FEEDBACK_SUBMITTED: "feedback_submitted";
84
+ readonly ACCOUNT_DELETED: "account_deleted";
85
+ readonly LANGUAGE_CHANGED: "language_changed";
86
+ readonly COMMENT_GUARD_MODE_CHANGED: "comment_guard_mode_changed";
87
+ readonly AI_TOOLS_REVOKE_REQUESTED: "ai_tools_revoke_requested";
78
88
  readonly NATIVE_SESSION_LAUNCHED: "native_session_launched";
79
89
  readonly NATIVE_SESSION_RENEWED: "native_session_renewed";
80
90
  readonly NATIVE_SESSION_ENDED: "native_session_ended";
@@ -165,7 +175,7 @@ export type ComposeStep = 'platforms' | 'content' | 'details' | 'review';
165
175
  * content, so neither may ride the event: only the class does. Same contract as
166
176
  * PublishFailureReason. Do NOT rename after shipping — wire values.
167
177
  */
168
- export type ComposeBlockReason = 'empty' | 'missing_fields' | 'over_limit' | 'media' | 'channel_blocked' | 'poll';
178
+ export type ComposeBlockReason = 'empty' | 'missing_fields' | 'over_limit' | 'media' | 'channel_blocked' | 'poll' | 'unconnected_platform';
169
179
  /**
170
180
  * Why a connect attempt ended without a connected account (spec 155 US3).
171
181
  *
@@ -196,9 +206,11 @@ export type ComposeBlockReason = 'empty' | 'missing_fields' | 'over_limit' | 'me
196
206
  export type OnboardingCompletedAt = 'connect' | 'install' | 'schedule';
197
207
  /** The finish lines, in the order the flow can reach them. */
198
208
  export declare const ONBOARDING_COMPLETED_AT: readonly ["connect", "install", "schedule"];
199
- export type ConnectBlockReason = 'paywall' | 'canceled' | 'failed' | 'returned_without_account' | 'personal_profile';
209
+ export type ConnectBlockReason = 'paywall' | 'canceled' | 'failed' | 'returned_without_account' | 'personal_profile' | 'api_failed' | 'timeout' | 'exception';
200
210
  /** The reason set, in the order the spec lists it. Iterable for contract tests and dashboards. */
201
- export declare const CONNECT_BLOCK_REASONS: readonly ["paywall", "canceled", "failed", "returned_without_account", "personal_profile"];
211
+ export declare const CONNECT_BLOCK_REASONS: readonly ["paywall", "canceled", "failed", "returned_without_account", "personal_profile", "api_failed", "timeout", "exception"];
212
+ export type SignInFailureReason = 'canceled' | 'missing_code' | 'invalid_provider' | 'no_email' | 'api_failed' | 'exception';
213
+ export declare const SIGN_IN_FAILURE_REASONS: readonly ["canceled", "missing_code", "invalid_provider", "no_email", "api_failed", "exception"];
202
214
  export type PublishFailureReason = 'auth' | 'rate_limit' | 'media' | 'content' | 'network' | 'platform' | 'unknown';
203
215
  /** Property shape per event. Keeps emitters honest across repos. */
204
216
  export interface EventProperties {
@@ -261,6 +273,10 @@ export interface EventProperties {
261
273
  account_count?: number;
262
274
  connection_ordinal?: number;
263
275
  };
276
+ [EVENTS.SOCIAL_SIGN_IN_FAILED]: {
277
+ platform?: string;
278
+ reason: SignInFailureReason;
279
+ };
264
280
  [EVENTS.COMPOSE_STARTED]: {
265
281
  source?: 'wizard' | 'quick';
266
282
  };
@@ -324,6 +340,15 @@ export interface EventProperties {
324
340
  plan: string;
325
341
  reason?: string;
326
342
  };
343
+ [EVENTS.BILLING_PORTAL_OPENED]: {
344
+ plan: string;
345
+ };
346
+ [EVENTS.SUBSCRIPTION_REACTIVATED]: {
347
+ plan: string;
348
+ };
349
+ [EVENTS.CHECKOUT_CONFIRMATION_TIMED_OUT]: {
350
+ had_session_id: boolean;
351
+ };
327
352
  [EVENTS.PAYWALL_SHOWN]: {
328
353
  surface: 'upgrade_modal' | 'pricing_page';
329
354
  trigger: 'user_click' | 'limit_reached' | 'connect_wall';
@@ -475,6 +500,22 @@ export interface EventProperties {
475
500
  enabled?: boolean;
476
501
  success: boolean;
477
502
  };
503
+ [EVENTS.THEME_CHANGED]: {
504
+ mode: 'light' | 'dark';
505
+ };
506
+ [EVENTS.FEEDBACK_SUBMITTED]: {
507
+ has_screenshot: boolean;
508
+ };
509
+ [EVENTS.ACCOUNT_DELETED]: {
510
+ tier: string;
511
+ };
512
+ [EVENTS.LANGUAGE_CHANGED]: {
513
+ language: 'en' | 'pl';
514
+ };
515
+ [EVENTS.COMMENT_GUARD_MODE_CHANGED]: {
516
+ mode: 'off' | 'flag-only' | 'auto-ignore';
517
+ };
518
+ [EVENTS.AI_TOOLS_REVOKE_REQUESTED]: Record<string, never>;
478
519
  [EVENTS.NATIVE_SESSION_LAUNCHED]: {
479
520
  outcome: 'restored' | 'none' | 'degraded';
480
521
  };
@@ -10,7 +10,7 @@
10
10
  * Do NOT rename events after they ship — historical data does not migrate.
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.LAUNCH_CONTEXT_PROPS = exports.PERSON_PROPS = exports.CONNECT_BLOCK_REASONS = exports.ONBOARDING_COMPLETED_AT = exports.EMAIL_PROPS = exports.EVENTS = void 0;
13
+ exports.LAUNCH_CONTEXT_PROPS = exports.PERSON_PROPS = exports.SIGN_IN_FAILURE_REASONS = exports.CONNECT_BLOCK_REASONS = exports.ONBOARDING_COMPLETED_AT = exports.EMAIL_PROPS = exports.EVENTS = void 0;
14
14
  /** Canonical PostHog event names. */
15
15
  exports.EVENTS = {
16
16
  // Acquisition (landing)
@@ -42,6 +42,12 @@ exports.EVENTS = {
42
42
  PROFILE_COMPLETED: 'profile_completed', // the /complete-profile step (influencers)
43
43
  LOGGED_IN: 'logged_in',
44
44
  SOCIAL_ACCOUNT_CONNECTED: 'social_account_connected',
45
+ // 164 G007: OAuthCallbackPage's sign-in branch (state !== 'web-connect')
46
+ // had zero tracking on any of its 5 failure outcomes -- LOGGED_IN only
47
+ // ever fires on success (PostHogIdentify.tsx, centrally, off the redux
48
+ // authed state), so failures need their own event rather than an
49
+ // overloaded success/failure LOGGED_IN.
50
+ SOCIAL_SIGN_IN_FAILED: 'social_sign_in_failed',
45
51
  COMPOSE_STARTED: 'compose_started',
46
52
  // AD2-1371: the composer was a black box between COMPOSE_STARTED and
47
53
  // POST_SCHEDULED. Measured 2026-08-28: of seven users who connected an
@@ -61,6 +67,17 @@ exports.EVENTS = {
61
67
  CHECKOUT_STARTED: 'checkout_started',
62
68
  SUBSCRIPTION_STARTED: 'subscription_started',
63
69
  SUBSCRIPTION_CANCELED: 'subscription_canceled',
70
+ // 164 G009 (SubscriptionSection had zero tracking on either action): client
71
+ // intent only — CANCEL_INITIATED above is already backend/webhook-owned
72
+ // (fires off the real `cancel_at_period_end` transition), so cancel itself
73
+ // is deliberately NOT duplicated here.
74
+ BILLING_PORTAL_OPENED: 'billing_portal_opened',
75
+ SUBSCRIPTION_REACTIVATED: 'subscription_reactivated',
76
+ // 164 (ComposePage -> PaymentProcessingPage): the client gave up polling for
77
+ // Stripe/webhook confirmation. Distinct from the backend-owned PAYMENT_FAILED
78
+ // (a real decline) -- this only means "we could not confirm in time", which
79
+ // may still resolve moments later.
80
+ CHECKOUT_CONFIRMATION_TIMED_OUT: 'checkout_confirmation_timed_out',
64
81
  // Measurement baseline (080) — paywall / onboarding / exit-intent (frontend-owned)
65
82
  PAYWALL_SHOWN: 'paywall_shown', // any monetization surface is shown (upgrade modal / pricing page)
66
83
  PAYWALL_DISMISSED: 'paywall_dismissed', // closed without converting
@@ -110,6 +127,19 @@ exports.EVENTS = {
110
127
  ACCOUNT_DISCONNECTED: 'account_disconnected',
111
128
  // Settings.
112
129
  NOTIFICATION_PREFERENCES_UPDATED: 'notification_preferences_updated',
130
+ // 164 G009: SettingsPage had `track`/`EVENTS` imported but never called --
131
+ // theme, feedback, and account-deletion all fired nothing. NOTIFICATION_PREFERENCES_UPDATED
132
+ // above already covers the marketing-emails toggle (its declared `{preference, enabled,
133
+ // success}` shape fits exactly; reused rather than adding a parallel event).
134
+ THEME_CHANGED: 'theme_changed',
135
+ FEEDBACK_SUBMITTED: 'feedback_submitted',
136
+ ACCOUNT_DELETED: 'account_deleted',
137
+ // Shared LanguageSwitcher (SignIn/SignUp/Settings) had the same zero-tracking gap.
138
+ LANGUAGE_CHANGED: 'language_changed',
139
+ // Behind SHOW_COMMENT_GUARD_SECTION / SHOW_AI_TOOLS_SECTION (both default OFF) —
140
+ // no real user can reach either today, but fixed for completeness/G009.
141
+ COMMENT_GUARD_MODE_CHANGED: 'comment_guard_mode_changed',
142
+ AI_TOOLS_REVOKE_REQUESTED: 'ai_tools_revoke_requested',
113
143
  // Native session lifecycle (159) — the iOS wrapper's Keychain-backed session,
114
144
  // reported so "does it survive" is a query instead of an inference from login
115
145
  // counts. Native only; the web session's posture is unchanged (contract Rule 5).
@@ -151,6 +181,17 @@ exports.CONNECT_BLOCK_REASONS = [
151
181
  'failed',
152
182
  'returned_without_account',
153
183
  'personal_profile',
184
+ 'api_failed',
185
+ 'timeout',
186
+ 'exception',
187
+ ];
188
+ exports.SIGN_IN_FAILURE_REASONS = [
189
+ 'canceled',
190
+ 'missing_code',
191
+ 'invalid_provider',
192
+ 'no_email',
193
+ 'api_failed',
194
+ 'exception',
154
195
  ];
155
196
  /** Canonical person property keys (set via identify / $set). */
156
197
  exports.PERSON_PROPS = {
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Agent domain — delegated authority DTOs (spec 163).
3
+ *
4
+ * The contract behind "an assistant acting for a creator". An agent client
5
+ * (today: ad2app-mcp) never holds a creator-equivalent credential. It holds a
6
+ * GRANT REFERENCE, and exchanges that reference — plus its own client
7
+ * credential — for a short-lived, scope-limited access token.
8
+ *
9
+ * The two halves are deliberately useless alone:
10
+ * - the grant id is not a secret (it authenticates nobody by itself)
11
+ * - the client credential proves WHICH CLIENT is asking, never WHOSE
12
+ * ACCOUNT may be acted on
13
+ *
14
+ * That property is spec FR-029, and it is what makes this a security
15
+ * improvement rather than a regression. See specs/speckit/
16
+ * 163-mcp-delegated-auth-direct-upload/contracts/agent-grant.md.
17
+ */
18
+ /**
19
+ * The complete set of capabilities an agent grant may carry.
20
+ *
21
+ * This list is a CEILING, not a starting point: it is exactly what the
22
+ * connector's existing tools already do. Widening it is a spec change, never a
23
+ * config change (spec Assumptions, "Granted capabilities").
24
+ */
25
+ export declare const AGENT_SCOPES: readonly ["social:post", "social:read", "social:analytics", "social:accounts", "media:upload", "social:billing"];
26
+ export type T_AgentScope = (typeof AGENT_SCOPES)[number];
27
+ /** Narrowing guard — an unrecognised scope must fail closed at issue time. */
28
+ export declare const isAgentScope: (value: string) => value is T_AgentScope;
29
+ /** Why a grant stopped being usable. Recorded for audit; the row survives. */
30
+ export type T_AgentGrantRevokedReason = 'creator' | 'admin' | 'credential_invalid' | 'superseded';
31
+ /**
32
+ * One creator's standing permission for one agent client.
33
+ *
34
+ * Lives until revoked — months. Not to be confused with the access token
35
+ * derived from it, which lives for minutes (see AgentAccessTokenDTO).
36
+ *
37
+ * Returned by GET /auth/agent-grants so a creator can answer "what has access
38
+ * to my account?". Never carries a token.
39
+ */
40
+ export declare class AgentGrantDTO {
41
+ /** The grant reference. Not a secret — useless without the client credential. */
42
+ id: string;
43
+ /** The acting client, e.g. "ad2app-mcp". */
44
+ clientId: string;
45
+ /** Granted capabilities. Never widened after issue; a wider grant is a new grant. */
46
+ scopes: T_AgentScope[];
47
+ createdAt: string;
48
+ /** Updated when an access token is issued, not on every request. */
49
+ lastUsedAt?: string | null;
50
+ revokedAt?: string | null;
51
+ revokedReason?: T_AgentGrantRevokedReason | null;
52
+ constructor(data: AgentGrantDTO);
53
+ }
54
+ /**
55
+ * The ONLY accepted way to ask for agent authority (spec FR-027).
56
+ *
57
+ * A creator's identity — user id, email, Firebase uid — is deliberately NOT a
58
+ * field here and must be REJECTED rather than ignored if one arrives. An
59
+ * earlier draft allowed issuing against a creator identity; that would have
60
+ * made the client credential a master key over every account in the database,
61
+ * including accounts that never connected an assistant (review 2026-09-12,
62
+ * finding F1).
63
+ */
64
+ export declare class AgentAccessTokenRequestDTO {
65
+ /** The grant reference, and nothing else. */
66
+ grantId: string;
67
+ /**
68
+ * Optional narrowing: request FEWER scopes than the grant carries. Must be a
69
+ * subset — a request for more than was granted fails closed.
70
+ */
71
+ scopes?: T_AgentScope[];
72
+ constructor(data: AgentAccessTokenRequestDTO);
73
+ }
74
+ /**
75
+ * A short-lived pass. Minutes, not months.
76
+ *
77
+ * The lifetime is the revocation window: renewal re-checks the grant, so a
78
+ * revoked grant stops producing tokens within one lifetime. That re-check is
79
+ * the entire point of keeping this short (spec FR-006, research R3).
80
+ */
81
+ export declare class AgentAccessTokenDTO {
82
+ accessToken: string;
83
+ /** Seconds. Also the documented worst-case revocation latency. */
84
+ expiresIn: number;
85
+ grantId: string;
86
+ scopes: T_AgentScope[];
87
+ constructor(data: AgentAccessTokenDTO);
88
+ }
89
+ /**
90
+ * The `act` claim: present ⇒ an agent is acting for the creator named by `sub`;
91
+ * absent ⇒ the creator is acting directly.
92
+ *
93
+ * This distinction is what the backend cannot currently make, and what lets
94
+ * agent-specific policy exist at all (spec FR-001).
95
+ */
96
+ export interface I_AgentActorClaim {
97
+ client_id: string;
98
+ grant_id: string;
99
+ }
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ /**
3
+ * Agent domain — delegated authority DTOs (spec 163).
4
+ *
5
+ * The contract behind "an assistant acting for a creator". An agent client
6
+ * (today: ad2app-mcp) never holds a creator-equivalent credential. It holds a
7
+ * GRANT REFERENCE, and exchanges that reference — plus its own client
8
+ * credential — for a short-lived, scope-limited access token.
9
+ *
10
+ * The two halves are deliberately useless alone:
11
+ * - the grant id is not a secret (it authenticates nobody by itself)
12
+ * - the client credential proves WHICH CLIENT is asking, never WHOSE
13
+ * ACCOUNT may be acted on
14
+ *
15
+ * That property is spec FR-029, and it is what makes this a security
16
+ * improvement rather than a regression. See specs/speckit/
17
+ * 163-mcp-delegated-auth-direct-upload/contracts/agent-grant.md.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.AgentAccessTokenDTO = exports.AgentAccessTokenRequestDTO = exports.AgentGrantDTO = exports.isAgentScope = exports.AGENT_SCOPES = void 0;
21
+ // ── Scopes ───────────────────────────────────────────────────────────────────
22
+ /**
23
+ * The complete set of capabilities an agent grant may carry.
24
+ *
25
+ * This list is a CEILING, not a starting point: it is exactly what the
26
+ * connector's existing tools already do. Widening it is a spec change, never a
27
+ * config change (spec Assumptions, "Granted capabilities").
28
+ */
29
+ exports.AGENT_SCOPES = [
30
+ /** create, update, cancel and retry posts */
31
+ 'social:post',
32
+ /** list posts, accounts and subscription status */
33
+ 'social:read',
34
+ /** read account and post analytics */
35
+ 'social:analytics',
36
+ /** connect and disconnect social accounts */
37
+ 'social:accounts',
38
+ /** request upload authority for the creator's own media */
39
+ 'media:upload',
40
+ /**
41
+ * Read subscription status and obtain an upgrade checkout URL.
42
+ *
43
+ * Deliberately separate from `social:read` (review L3): the checkout route is
44
+ * a POST that creates a Stripe session. It moves no money — the creator still
45
+ * pays on Stripe's own page — but a scope named "read" guarding it is the
46
+ * kind of mismatch that gets copied to somewhere it does matter.
47
+ */
48
+ 'social:billing',
49
+ ];
50
+ /** Narrowing guard — an unrecognised scope must fail closed at issue time. */
51
+ const isAgentScope = (value) => exports.AGENT_SCOPES.includes(value);
52
+ exports.isAgentScope = isAgentScope;
53
+ // ── AgentGrantDTO ────────────────────────────────────────────────────────────
54
+ /**
55
+ * One creator's standing permission for one agent client.
56
+ *
57
+ * Lives until revoked — months. Not to be confused with the access token
58
+ * derived from it, which lives for minutes (see AgentAccessTokenDTO).
59
+ *
60
+ * Returned by GET /auth/agent-grants so a creator can answer "what has access
61
+ * to my account?". Never carries a token.
62
+ */
63
+ class AgentGrantDTO {
64
+ constructor(data) {
65
+ this.id = data.id;
66
+ this.clientId = data.clientId;
67
+ this.scopes = data.scopes;
68
+ this.createdAt = data.createdAt;
69
+ this.lastUsedAt = data.lastUsedAt ?? null;
70
+ this.revokedAt = data.revokedAt ?? null;
71
+ this.revokedReason = data.revokedReason ?? null;
72
+ }
73
+ }
74
+ exports.AgentGrantDTO = AgentGrantDTO;
75
+ // ── AgentAccessTokenRequestDTO ───────────────────────────────────────────────
76
+ /**
77
+ * The ONLY accepted way to ask for agent authority (spec FR-027).
78
+ *
79
+ * A creator's identity — user id, email, Firebase uid — is deliberately NOT a
80
+ * field here and must be REJECTED rather than ignored if one arrives. An
81
+ * earlier draft allowed issuing against a creator identity; that would have
82
+ * made the client credential a master key over every account in the database,
83
+ * including accounts that never connected an assistant (review 2026-09-12,
84
+ * finding F1).
85
+ */
86
+ class AgentAccessTokenRequestDTO {
87
+ constructor(data) {
88
+ this.grantId = data.grantId;
89
+ this.scopes = data.scopes;
90
+ }
91
+ }
92
+ exports.AgentAccessTokenRequestDTO = AgentAccessTokenRequestDTO;
93
+ // ── AgentAccessTokenDTO ──────────────────────────────────────────────────────
94
+ /**
95
+ * A short-lived pass. Minutes, not months.
96
+ *
97
+ * The lifetime is the revocation window: renewal re-checks the grant, so a
98
+ * revoked grant stops producing tokens within one lifetime. That re-check is
99
+ * the entire point of keeping this short (spec FR-006, research R3).
100
+ */
101
+ class AgentAccessTokenDTO {
102
+ constructor(data) {
103
+ this.accessToken = data.accessToken;
104
+ this.expiresIn = data.expiresIn;
105
+ this.grantId = data.grantId;
106
+ this.scopes = data.scopes;
107
+ }
108
+ }
109
+ exports.AgentAccessTokenDTO = AgentAccessTokenDTO;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Agent domain — media upload DTOs (spec 163).
3
+ *
4
+ * Replaces the browser hand-off as the DEFAULT way an agent adds a creator's
5
+ * media. The bytes travel from wherever the file already is — the agent
6
+ * client's own machine — straight to storage. They do not pass through a
7
+ * browser, and the creator is never asked to confirm an upload finished.
8
+ *
9
+ * See specs/speckit/163-mcp-delegated-auth-direct-upload/contracts/
10
+ * media-upload.md for how a path is chosen (attempt-then-fallback; there is no
11
+ * capability check, because MCP exposes none).
12
+ */
13
+ /** One file's metadata. Bytes are NOT part of this — see the direct path. */
14
+ export declare class AgentUploadFileRequestDTO {
15
+ fileName: string;
16
+ /** Must be on the backend's MIME allowlist; checked per file, not per batch. */
17
+ fileType: string;
18
+ fileSize: number;
19
+ constructor(data: AgentUploadFileRequestDTO);
20
+ }
21
+ /**
22
+ * A batch. Deliberately unbounded by any fixed ceiling (spec FR-013).
23
+ *
24
+ * The old browser path capped a session at ten files with one upload in flight;
25
+ * a 77-file batch therefore cost eight browser round-trips. That ceiling bounded
26
+ * a web page, not a creator, and does not apply here.
27
+ *
28
+ * NOTE: what may be PUBLISHED together is a separate limit — a carousel is at
29
+ * most ten anywhere. This governs uploading only.
30
+ */
31
+ export declare class AgentUploadBatchRequestDTO {
32
+ files: AgentUploadFileRequestDTO[];
33
+ constructor(data: AgentUploadBatchRequestDTO);
34
+ }
35
+ /** Why one file was refused. The rest of the batch is unaffected (FR-015). */
36
+ export type T_AgentUploadRejectionReason = 'file_too_large' | 'unsupported_type' | 'invalid_name' | 'invalid_size';
37
+ /** One accepted file: where to put the bytes, and how to refer to it afterwards. */
38
+ export declare class AgentUploadTargetDTO {
39
+ fileName: string;
40
+ /**
41
+ * The opaque handle used when composing a post, as `media:<id>`.
42
+ * Neither the creator nor the model ever handles a raw storage location.
43
+ */
44
+ mediaRef: string;
45
+ /**
46
+ * Single-purpose, short-lived write authority for THIS file only.
47
+ * Not general storage access, not account access.
48
+ */
49
+ uploadUrl: string;
50
+ /** Seconds. */
51
+ expiresIn: number;
52
+ constructor(data: AgentUploadTargetDTO);
53
+ }
54
+ /** One refused file, named so the creator knows which (FR-015). */
55
+ export declare class AgentUploadRejectionDTO {
56
+ fileName: string;
57
+ reason: T_AgentUploadRejectionReason;
58
+ /** Plain language, for the creator. No status codes, no internal rule names. */
59
+ message: string;
60
+ constructor(data: AgentUploadRejectionDTO);
61
+ }
62
+ /**
63
+ * A batch result. Per-file outcomes, never a single aggregate verdict.
64
+ *
65
+ * `nextCursor` exists because a large batch's response has to fit in one tool
66
+ * result. The 77-file case is measured rather than assumed (spec FR-030,
67
+ * review finding F4); when a flat list does not fit, entries are paged.
68
+ */
69
+ export declare class AgentUploadBatchResponseDTO {
70
+ targets: AgentUploadTargetDTO[];
71
+ rejections: AgentUploadRejectionDTO[];
72
+ /** Present when more entries remain; pass back to fetch the next page. */
73
+ nextCursor?: string | null;
74
+ constructor(data: AgentUploadBatchResponseDTO);
75
+ }
76
+ /**
77
+ * Per-ref confirmation that the bytes actually landed.
78
+ *
79
+ * `ready` means VERIFIED PRESENT IN STORAGE, server-side — never "the host said
80
+ * so" (spec FR-012). Media that is not ready cannot be attached to a post
81
+ * (FR-017), which is what keeps a failed upload from becoming a published post
82
+ * with missing images.
83
+ */
84
+ export declare class AgentUploadConfirmationDTO {
85
+ mediaRef: string;
86
+ ready: boolean;
87
+ /** Set when not ready, in plain language the creator can act on. */
88
+ message?: string;
89
+ constructor(data: AgentUploadConfirmationDTO);
90
+ }
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ /**
3
+ * Agent domain — media upload DTOs (spec 163).
4
+ *
5
+ * Replaces the browser hand-off as the DEFAULT way an agent adds a creator's
6
+ * media. The bytes travel from wherever the file already is — the agent
7
+ * client's own machine — straight to storage. They do not pass through a
8
+ * browser, and the creator is never asked to confirm an upload finished.
9
+ *
10
+ * See specs/speckit/163-mcp-delegated-auth-direct-upload/contracts/
11
+ * media-upload.md for how a path is chosen (attempt-then-fallback; there is no
12
+ * capability check, because MCP exposes none).
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.AgentUploadConfirmationDTO = exports.AgentUploadBatchResponseDTO = exports.AgentUploadRejectionDTO = exports.AgentUploadTargetDTO = exports.AgentUploadBatchRequestDTO = exports.AgentUploadFileRequestDTO = void 0;
16
+ // ── Requests ─────────────────────────────────────────────────────────────────
17
+ /** One file's metadata. Bytes are NOT part of this — see the direct path. */
18
+ class AgentUploadFileRequestDTO {
19
+ constructor(data) {
20
+ this.fileName = data.fileName;
21
+ this.fileType = data.fileType;
22
+ this.fileSize = data.fileSize;
23
+ }
24
+ }
25
+ exports.AgentUploadFileRequestDTO = AgentUploadFileRequestDTO;
26
+ /**
27
+ * A batch. Deliberately unbounded by any fixed ceiling (spec FR-013).
28
+ *
29
+ * The old browser path capped a session at ten files with one upload in flight;
30
+ * a 77-file batch therefore cost eight browser round-trips. That ceiling bounded
31
+ * a web page, not a creator, and does not apply here.
32
+ *
33
+ * NOTE: what may be PUBLISHED together is a separate limit — a carousel is at
34
+ * most ten anywhere. This governs uploading only.
35
+ */
36
+ class AgentUploadBatchRequestDTO {
37
+ constructor(data) {
38
+ this.files = data.files;
39
+ }
40
+ }
41
+ exports.AgentUploadBatchRequestDTO = AgentUploadBatchRequestDTO;
42
+ /** One accepted file: where to put the bytes, and how to refer to it afterwards. */
43
+ class AgentUploadTargetDTO {
44
+ constructor(data) {
45
+ this.fileName = data.fileName;
46
+ this.mediaRef = data.mediaRef;
47
+ this.uploadUrl = data.uploadUrl;
48
+ this.expiresIn = data.expiresIn;
49
+ }
50
+ }
51
+ exports.AgentUploadTargetDTO = AgentUploadTargetDTO;
52
+ /** One refused file, named so the creator knows which (FR-015). */
53
+ class AgentUploadRejectionDTO {
54
+ constructor(data) {
55
+ this.fileName = data.fileName;
56
+ this.reason = data.reason;
57
+ this.message = data.message;
58
+ }
59
+ }
60
+ exports.AgentUploadRejectionDTO = AgentUploadRejectionDTO;
61
+ /**
62
+ * A batch result. Per-file outcomes, never a single aggregate verdict.
63
+ *
64
+ * `nextCursor` exists because a large batch's response has to fit in one tool
65
+ * result. The 77-file case is measured rather than assumed (spec FR-030,
66
+ * review finding F4); when a flat list does not fit, entries are paged.
67
+ */
68
+ class AgentUploadBatchResponseDTO {
69
+ constructor(data) {
70
+ this.targets = data.targets;
71
+ this.rejections = data.rejections;
72
+ this.nextCursor = data.nextCursor ?? null;
73
+ }
74
+ }
75
+ exports.AgentUploadBatchResponseDTO = AgentUploadBatchResponseDTO;
76
+ // ── Confirmation ─────────────────────────────────────────────────────────────
77
+ /**
78
+ * Per-ref confirmation that the bytes actually landed.
79
+ *
80
+ * `ready` means VERIFIED PRESENT IN STORAGE, server-side — never "the host said
81
+ * so" (spec FR-012). Media that is not ready cannot be attached to a post
82
+ * (FR-017), which is what keeps a failed upload from becoming a published post
83
+ * with missing images.
84
+ */
85
+ class AgentUploadConfirmationDTO {
86
+ constructor(data) {
87
+ this.mediaRef = data.mediaRef;
88
+ this.ready = data.ready;
89
+ this.message = data.message;
90
+ }
91
+ }
92
+ exports.AgentUploadConfirmationDTO = AgentUploadConfirmationDTO;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Agent domain — barrel export (spec 163).
3
+ *
4
+ * Available from 'ad2app-lib/types' via:
5
+ * import { AgentGrantDTO, AGENT_SCOPES, ... } from 'ad2app-lib/types';
6
+ */
7
+ export * from './I_AgentGrant';
8
+ export * from './I_AgentMediaUpload';
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ /**
3
+ * Agent domain — barrel export (spec 163).
4
+ *
5
+ * Available from 'ad2app-lib/types' via:
6
+ * import { AgentGrantDTO, AGENT_SCOPES, ... } from 'ad2app-lib/types';
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
20
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
21
+ };
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ __exportStar(require("./I_AgentGrant"), exports);
24
+ __exportStar(require("./I_AgentMediaUpload"), exports);
@@ -35,4 +35,5 @@ export * from "./I_Collaboration";
35
35
  export * from "./I_Publish";
36
36
  export * from "./I_SM_Platform";
37
37
  export * from "./scheduling";
38
+ export * from "./agent";
38
39
  export * from "./I_AccessDenial";
@@ -52,5 +52,7 @@ __exportStar(require("./I_Publish"), exports);
52
52
  __exportStar(require("./I_SM_Platform"), exports);
53
53
  // ── Scheduling domain ─────────────────────────────────────────────────────────
54
54
  __exportStar(require("./scheduling"), exports);
55
+ // ── Agent domain (delegated authority + direct media upload, spec 163) ───────
56
+ __exportStar(require("./agent"), exports);
55
57
  // ── Access control ────────────────────────────────────────────────────────────
56
58
  __exportStar(require("./I_AccessDenial"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ad2app-lib",
3
- "version": "1.37.0",
3
+ "version": "1.39.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "commonjs",
@@ -13,13 +13,19 @@ import { test } from 'node:test';
13
13
  import { CONNECT_BLOCK_REASONS, EVENTS } from './index';
14
14
  import type { ConnectBlockReason, EventProperties } from './index';
15
15
 
16
- test('the reason set is the five causes, as wire values', () => {
16
+ test('the reason set is the eight causes, as wire values', () => {
17
+ // 164 G001: widened from five to eight — 'api_failed'/'timeout'/'exception'
18
+ // cover connectAccount() itself failing before any redirect ever happens
19
+ // (confirmed live 2026-09-07 as the one previously-untracked branch).
17
20
  assert.deepEqual([...CONNECT_BLOCK_REASONS], [
18
21
  'paywall',
19
22
  'canceled',
20
23
  'failed',
21
24
  'returned_without_account',
22
25
  'personal_profile',
26
+ 'api_failed',
27
+ 'timeout',
28
+ 'exception',
23
29
  ]);
24
30
  });
25
31
 
@@ -90,6 +90,8 @@ const EVENT_PROPERTY_WITNESS: { [E in keyof EventProperties]: EventProperties[E]
90
90
  // 'email' | 'google' union (compile-time, enforced by `tsc --noEmit`).
91
91
  [EVENTS.LOGGED_IN]: { method: "apple" },
92
92
  [EVENTS.SOCIAL_ACCOUNT_CONNECTED]: { platform: "instagram" },
93
+ // 164 G007
94
+ [EVENTS.SOCIAL_SIGN_IN_FAILED]: { platform: "facebook", reason: "canceled" },
93
95
  [EVENTS.COMPOSE_STARTED]: {},
94
96
  [EVENTS.COMPOSE_STEP_VIEWED]: { step: "content" },
95
97
  [EVENTS.COMPOSE_BLOCKED]: { step: "content", reasons: ["missing_fields"], reason_count: 1 },
@@ -102,6 +104,11 @@ const EVENT_PROPERTY_WITNESS: { [E in keyof EventProperties]: EventProperties[E]
102
104
  [EVENTS.CHECKOUT_STARTED]: { plan: "starter" },
103
105
  [EVENTS.SUBSCRIPTION_STARTED]: { plan: "starter" },
104
106
  [EVENTS.SUBSCRIPTION_CANCELED]: { plan: "starter" },
107
+ // 164 G011
108
+ [EVENTS.BILLING_PORTAL_OPENED]: { plan: "pro" },
109
+ [EVENTS.SUBSCRIPTION_REACTIVATED]: { plan: "pro" },
110
+ // 164 (PaymentProcessingPage)
111
+ [EVENTS.CHECKOUT_CONFIRMATION_TIMED_OUT]: { had_session_id: true },
105
112
  // 080 measurement baseline
106
113
  [EVENTS.PAYWALL_SHOWN]: { surface: "upgrade_modal", trigger: "user_click", hit_number: 1, app_locale: "en" },
107
114
  [EVENTS.PAYWALL_DISMISSED]: { surface: "upgrade_modal", trigger: "user_click", hit_number: 1, app_locale: "en" },
@@ -139,6 +146,14 @@ const EVENT_PROPERTY_WITNESS: { [E in keyof EventProperties]: EventProperties[E]
139
146
  [EVENTS.NATIVE_SESSION_LAUNCHED]: { outcome: "restored" },
140
147
  [EVENTS.NATIVE_SESSION_RENEWED]: {},
141
148
  [EVENTS.NATIVE_SESSION_ENDED]: { cause: "rejected" },
149
+ // 164 G009 — SettingsPage had track/EVENTS imported but never called
150
+ [EVENTS.THEME_CHANGED]: { mode: "dark" },
151
+ [EVENTS.FEEDBACK_SUBMITTED]: { has_screenshot: false },
152
+ [EVENTS.ACCOUNT_DELETED]: { tier: "pro" },
153
+ [EVENTS.LANGUAGE_CHANGED]: { language: "pl" },
154
+ // 164 G012 — both behind default-OFF flags, fixed for completeness
155
+ [EVENTS.COMMENT_GUARD_MODE_CHANGED]: { mode: "flag-only" },
156
+ [EVENTS.AI_TOOLS_REVOKE_REQUESTED]: {},
142
157
  };
143
158
 
144
159
  test("EVENTS values are 1:1 with EventProperties keys (no missing or typo'd event)", () => {
@@ -46,6 +46,12 @@ export const EVENTS = {
46
46
  PROFILE_COMPLETED: 'profile_completed', // the /complete-profile step (influencers)
47
47
  LOGGED_IN: 'logged_in',
48
48
  SOCIAL_ACCOUNT_CONNECTED: 'social_account_connected',
49
+ // 164 G007: OAuthCallbackPage's sign-in branch (state !== 'web-connect')
50
+ // had zero tracking on any of its 5 failure outcomes -- LOGGED_IN only
51
+ // ever fires on success (PostHogIdentify.tsx, centrally, off the redux
52
+ // authed state), so failures need their own event rather than an
53
+ // overloaded success/failure LOGGED_IN.
54
+ SOCIAL_SIGN_IN_FAILED: 'social_sign_in_failed',
49
55
  COMPOSE_STARTED: 'compose_started',
50
56
  // AD2-1371: the composer was a black box between COMPOSE_STARTED and
51
57
  // POST_SCHEDULED. Measured 2026-08-28: of seven users who connected an
@@ -66,6 +72,17 @@ export const EVENTS = {
66
72
  CHECKOUT_STARTED: 'checkout_started',
67
73
  SUBSCRIPTION_STARTED: 'subscription_started',
68
74
  SUBSCRIPTION_CANCELED: 'subscription_canceled',
75
+ // 164 G009 (SubscriptionSection had zero tracking on either action): client
76
+ // intent only — CANCEL_INITIATED above is already backend/webhook-owned
77
+ // (fires off the real `cancel_at_period_end` transition), so cancel itself
78
+ // is deliberately NOT duplicated here.
79
+ BILLING_PORTAL_OPENED: 'billing_portal_opened',
80
+ SUBSCRIPTION_REACTIVATED: 'subscription_reactivated',
81
+ // 164 (ComposePage -> PaymentProcessingPage): the client gave up polling for
82
+ // Stripe/webhook confirmation. Distinct from the backend-owned PAYMENT_FAILED
83
+ // (a real decline) -- this only means "we could not confirm in time", which
84
+ // may still resolve moments later.
85
+ CHECKOUT_CONFIRMATION_TIMED_OUT: 'checkout_confirmation_timed_out',
69
86
 
70
87
  // Measurement baseline (080) — paywall / onboarding / exit-intent (frontend-owned)
71
88
  PAYWALL_SHOWN: 'paywall_shown', // any monetization surface is shown (upgrade modal / pricing page)
@@ -121,6 +138,19 @@ export const EVENTS = {
121
138
 
122
139
  // Settings.
123
140
  NOTIFICATION_PREFERENCES_UPDATED: 'notification_preferences_updated',
141
+ // 164 G009: SettingsPage had `track`/`EVENTS` imported but never called --
142
+ // theme, feedback, and account-deletion all fired nothing. NOTIFICATION_PREFERENCES_UPDATED
143
+ // above already covers the marketing-emails toggle (its declared `{preference, enabled,
144
+ // success}` shape fits exactly; reused rather than adding a parallel event).
145
+ THEME_CHANGED: 'theme_changed',
146
+ FEEDBACK_SUBMITTED: 'feedback_submitted',
147
+ ACCOUNT_DELETED: 'account_deleted',
148
+ // Shared LanguageSwitcher (SignIn/SignUp/Settings) had the same zero-tracking gap.
149
+ LANGUAGE_CHANGED: 'language_changed',
150
+ // Behind SHOW_COMMENT_GUARD_SECTION / SHOW_AI_TOOLS_SECTION (both default OFF) —
151
+ // no real user can reach either today, but fixed for completeness/G009.
152
+ COMMENT_GUARD_MODE_CHANGED: 'comment_guard_mode_changed',
153
+ AI_TOOLS_REVOKE_REQUESTED: 'ai_tools_revoke_requested',
124
154
 
125
155
  // Native session lifecycle (159) — the iOS wrapper's Keychain-backed session,
126
156
  // reported so "does it survive" is a query instead of an inference from login
@@ -242,7 +272,12 @@ export type ComposeBlockReason =
242
272
  | 'over_limit' // caption longer than a selected platform allows
243
273
  | 'media' // media rejected (format, size, duration, count)
244
274
  | 'channel_blocked' // the channel itself refused (e.g. a failed prerequisite read)
245
- | 'poll'; // an incomplete or unpostable poll
275
+ | 'poll' // an incomplete or unpostable poll
276
+ // 164 G002: picking an unconnected platform at step 1 (Platforms) redirects
277
+ // straight to /accounts with zero analytics -- same "composer refused to
278
+ // let you proceed" shape as the publish-readiness reasons above, just one
279
+ // step earlier, so it reuses this event/reason set rather than a new event.
280
+ | 'unconnected_platform';
246
281
 
247
282
  /**
248
283
  * Why a connect attempt ended without a connected account (spec 155 US3).
@@ -285,7 +320,14 @@ export type ConnectBlockReason =
285
320
  | 'canceled'
286
321
  | 'failed'
287
322
  | 'returned_without_account'
288
- | 'personal_profile';
323
+ | 'personal_profile'
324
+ // 164 G001: 'failed' above is the POST-redirect case (backend appends
325
+ // ?connect=failed once the provider has already handed control back).
326
+ // These three cover the connectAccount() API call itself never getting a
327
+ // redirect at all — confirmed live 2026-09-07 as the one untracked branch.
328
+ | 'api_failed'
329
+ | 'timeout'
330
+ | 'exception';
289
331
 
290
332
  /** The reason set, in the order the spec lists it. Iterable for contract tests and dashboards. */
291
333
  export const CONNECT_BLOCK_REASONS = [
@@ -294,8 +336,31 @@ export const CONNECT_BLOCK_REASONS = [
294
336
  'failed',
295
337
  'returned_without_account',
296
338
  'personal_profile',
339
+ 'api_failed',
340
+ 'timeout',
341
+ 'exception',
297
342
  ] as const satisfies readonly ConnectBlockReason[];
298
343
 
344
+ // 164 G007: covers OAuthCallbackPage's social sign-in branch (as opposed to
345
+ // ConnectBlockReason, which covers its account-connect branch) -- distinct
346
+ // enums because the two flows fail for different reasons.
347
+ export type SignInFailureReason =
348
+ | 'canceled' // provider returned ?error= (user denied / backed out)
349
+ | 'missing_code' // callback landed with no ?code= at all
350
+ | 'invalid_provider' // :provider route param missing/unrecognized
351
+ | 'no_email' // provider granted no email (Facebook/TikTok phone-only) -- AD2-1252
352
+ | 'api_failed' // socialSignIn() resolved without an accessToken
353
+ | 'exception'; // socialSignIn() itself threw
354
+
355
+ export const SIGN_IN_FAILURE_REASONS = [
356
+ 'canceled',
357
+ 'missing_code',
358
+ 'invalid_provider',
359
+ 'no_email',
360
+ 'api_failed',
361
+ 'exception',
362
+ ] as const satisfies readonly SignInFailureReason[];
363
+
299
364
  export type PublishFailureReason =
300
365
  | 'auth' // token expired / revoked / missing scope / permission denied
301
366
  | 'rate_limit' // provider throttled us (429 / quota)
@@ -352,6 +417,7 @@ export interface EventProperties {
352
417
  // 080: 1 = first-ever account, 2 = second… — the "second action" activation hook.
353
418
  connection_ordinal?: number;
354
419
  };
420
+ [EVENTS.SOCIAL_SIGN_IN_FAILED]: { platform?: string; reason: SignInFailureReason };
355
421
  [EVENTS.COMPOSE_STARTED]: { source?: 'wizard' | 'quick' };
356
422
  [EVENTS.COMPOSE_STEP_VIEWED]: { step: ComposeStep };
357
423
  [EVENTS.COMPOSE_BLOCKED]: {
@@ -419,6 +485,9 @@ export interface EventProperties {
419
485
  trigger_source?: TriggerSource; // 080
420
486
  };
421
487
  [EVENTS.SUBSCRIPTION_CANCELED]: { plan: string; reason?: string };
488
+ [EVENTS.BILLING_PORTAL_OPENED]: { plan: string };
489
+ [EVENTS.SUBSCRIPTION_REACTIVATED]: { plan: string };
490
+ [EVENTS.CHECKOUT_CONFIRMATION_TIMED_OUT]: { had_session_id: boolean };
422
491
 
423
492
  // ── Measurement baseline (080) — new event property shapes ──────────────────
424
493
  // Paywall surfaces (frontend). hit_number is the analytical truth (an event
@@ -511,6 +580,12 @@ export interface EventProperties {
511
580
 
512
581
  // Settings — which preference toggled and its new state.
513
582
  [EVENTS.NOTIFICATION_PREFERENCES_UPDATED]: { preference?: string; enabled?: boolean; success: boolean };
583
+ [EVENTS.THEME_CHANGED]: { mode: 'light' | 'dark' };
584
+ [EVENTS.FEEDBACK_SUBMITTED]: { has_screenshot: boolean };
585
+ [EVENTS.ACCOUNT_DELETED]: { tier: string };
586
+ [EVENTS.LANGUAGE_CHANGED]: { language: 'en' | 'pl' };
587
+ [EVENTS.COMMENT_GUARD_MODE_CHANGED]: { mode: 'off' | 'flag-only' | 'auto-ignore' };
588
+ [EVENTS.AI_TOOLS_REVOKE_REQUESTED]: Record<string, never>;
514
589
 
515
590
  // Native session lifecycle (159, contract session-reports.md). `outcome`
516
591
  // covers all three launch reports in one event (restored / none / degraded)
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Agent domain — delegated authority DTOs (spec 163).
3
+ *
4
+ * The contract behind "an assistant acting for a creator". An agent client
5
+ * (today: ad2app-mcp) never holds a creator-equivalent credential. It holds a
6
+ * GRANT REFERENCE, and exchanges that reference — plus its own client
7
+ * credential — for a short-lived, scope-limited access token.
8
+ *
9
+ * The two halves are deliberately useless alone:
10
+ * - the grant id is not a secret (it authenticates nobody by itself)
11
+ * - the client credential proves WHICH CLIENT is asking, never WHOSE
12
+ * ACCOUNT may be acted on
13
+ *
14
+ * That property is spec FR-029, and it is what makes this a security
15
+ * improvement rather than a regression. See specs/speckit/
16
+ * 163-mcp-delegated-auth-direct-upload/contracts/agent-grant.md.
17
+ */
18
+
19
+ // ── Scopes ───────────────────────────────────────────────────────────────────
20
+
21
+ /**
22
+ * The complete set of capabilities an agent grant may carry.
23
+ *
24
+ * This list is a CEILING, not a starting point: it is exactly what the
25
+ * connector's existing tools already do. Widening it is a spec change, never a
26
+ * config change (spec Assumptions, "Granted capabilities").
27
+ */
28
+ export const AGENT_SCOPES = [
29
+ /** create, update, cancel and retry posts */
30
+ 'social:post',
31
+ /** list posts, accounts and subscription status */
32
+ 'social:read',
33
+ /** read account and post analytics */
34
+ 'social:analytics',
35
+ /** connect and disconnect social accounts */
36
+ 'social:accounts',
37
+ /** request upload authority for the creator's own media */
38
+ 'media:upload',
39
+ /**
40
+ * Read subscription status and obtain an upgrade checkout URL.
41
+ *
42
+ * Deliberately separate from `social:read` (review L3): the checkout route is
43
+ * a POST that creates a Stripe session. It moves no money — the creator still
44
+ * pays on Stripe's own page — but a scope named "read" guarding it is the
45
+ * kind of mismatch that gets copied to somewhere it does matter.
46
+ */
47
+ 'social:billing',
48
+ ] as const;
49
+
50
+ export type T_AgentScope = (typeof AGENT_SCOPES)[number];
51
+
52
+ /** Narrowing guard — an unrecognised scope must fail closed at issue time. */
53
+ export const isAgentScope = (value: string): value is T_AgentScope =>
54
+ (AGENT_SCOPES as readonly string[]).includes(value);
55
+
56
+ /** Why a grant stopped being usable. Recorded for audit; the row survives. */
57
+ export type T_AgentGrantRevokedReason =
58
+ | 'creator'
59
+ | 'admin'
60
+ | 'credential_invalid'
61
+ | 'superseded';
62
+
63
+ // ── AgentGrantDTO ────────────────────────────────────────────────────────────
64
+
65
+ /**
66
+ * One creator's standing permission for one agent client.
67
+ *
68
+ * Lives until revoked — months. Not to be confused with the access token
69
+ * derived from it, which lives for minutes (see AgentAccessTokenDTO).
70
+ *
71
+ * Returned by GET /auth/agent-grants so a creator can answer "what has access
72
+ * to my account?". Never carries a token.
73
+ */
74
+ export class AgentGrantDTO {
75
+ /** The grant reference. Not a secret — useless without the client credential. */
76
+ id: string;
77
+ /** The acting client, e.g. "ad2app-mcp". */
78
+ clientId: string;
79
+ /** Granted capabilities. Never widened after issue; a wider grant is a new grant. */
80
+ scopes: T_AgentScope[];
81
+ createdAt: string;
82
+ /** Updated when an access token is issued, not on every request. */
83
+ lastUsedAt?: string | null;
84
+ revokedAt?: string | null;
85
+ revokedReason?: T_AgentGrantRevokedReason | null;
86
+
87
+ constructor(data: AgentGrantDTO) {
88
+ this.id = data.id;
89
+ this.clientId = data.clientId;
90
+ this.scopes = data.scopes;
91
+ this.createdAt = data.createdAt;
92
+ this.lastUsedAt = data.lastUsedAt ?? null;
93
+ this.revokedAt = data.revokedAt ?? null;
94
+ this.revokedReason = data.revokedReason ?? null;
95
+ }
96
+ }
97
+
98
+ // ── AgentAccessTokenRequestDTO ───────────────────────────────────────────────
99
+
100
+ /**
101
+ * The ONLY accepted way to ask for agent authority (spec FR-027).
102
+ *
103
+ * A creator's identity — user id, email, Firebase uid — is deliberately NOT a
104
+ * field here and must be REJECTED rather than ignored if one arrives. An
105
+ * earlier draft allowed issuing against a creator identity; that would have
106
+ * made the client credential a master key over every account in the database,
107
+ * including accounts that never connected an assistant (review 2026-09-12,
108
+ * finding F1).
109
+ */
110
+ export class AgentAccessTokenRequestDTO {
111
+ /** The grant reference, and nothing else. */
112
+ grantId: string;
113
+ /**
114
+ * Optional narrowing: request FEWER scopes than the grant carries. Must be a
115
+ * subset — a request for more than was granted fails closed.
116
+ */
117
+ scopes?: T_AgentScope[];
118
+
119
+ constructor(data: AgentAccessTokenRequestDTO) {
120
+ this.grantId = data.grantId;
121
+ this.scopes = data.scopes;
122
+ }
123
+ }
124
+
125
+ // ── AgentAccessTokenDTO ──────────────────────────────────────────────────────
126
+
127
+ /**
128
+ * A short-lived pass. Minutes, not months.
129
+ *
130
+ * The lifetime is the revocation window: renewal re-checks the grant, so a
131
+ * revoked grant stops producing tokens within one lifetime. That re-check is
132
+ * the entire point of keeping this short (spec FR-006, research R3).
133
+ */
134
+ export class AgentAccessTokenDTO {
135
+ accessToken: string;
136
+ /** Seconds. Also the documented worst-case revocation latency. */
137
+ expiresIn: number;
138
+ grantId: string;
139
+ scopes: T_AgentScope[];
140
+
141
+ constructor(data: AgentAccessTokenDTO) {
142
+ this.accessToken = data.accessToken;
143
+ this.expiresIn = data.expiresIn;
144
+ this.grantId = data.grantId;
145
+ this.scopes = data.scopes;
146
+ }
147
+ }
148
+
149
+ // ── Token claim shape ────────────────────────────────────────────────────────
150
+
151
+ /**
152
+ * The `act` claim: present ⇒ an agent is acting for the creator named by `sub`;
153
+ * absent ⇒ the creator is acting directly.
154
+ *
155
+ * This distinction is what the backend cannot currently make, and what lets
156
+ * agent-specific policy exist at all (spec FR-001).
157
+ */
158
+ export interface I_AgentActorClaim {
159
+ client_id: string;
160
+ grant_id: string;
161
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Agent domain — media upload DTOs (spec 163).
3
+ *
4
+ * Replaces the browser hand-off as the DEFAULT way an agent adds a creator's
5
+ * media. The bytes travel from wherever the file already is — the agent
6
+ * client's own machine — straight to storage. They do not pass through a
7
+ * browser, and the creator is never asked to confirm an upload finished.
8
+ *
9
+ * See specs/speckit/163-mcp-delegated-auth-direct-upload/contracts/
10
+ * media-upload.md for how a path is chosen (attempt-then-fallback; there is no
11
+ * capability check, because MCP exposes none).
12
+ */
13
+
14
+ // ── Requests ─────────────────────────────────────────────────────────────────
15
+
16
+ /** One file's metadata. Bytes are NOT part of this — see the direct path. */
17
+ export class AgentUploadFileRequestDTO {
18
+ fileName: string;
19
+ /** Must be on the backend's MIME allowlist; checked per file, not per batch. */
20
+ fileType: string;
21
+ fileSize: number;
22
+
23
+ constructor(data: AgentUploadFileRequestDTO) {
24
+ this.fileName = data.fileName;
25
+ this.fileType = data.fileType;
26
+ this.fileSize = data.fileSize;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * A batch. Deliberately unbounded by any fixed ceiling (spec FR-013).
32
+ *
33
+ * The old browser path capped a session at ten files with one upload in flight;
34
+ * a 77-file batch therefore cost eight browser round-trips. That ceiling bounded
35
+ * a web page, not a creator, and does not apply here.
36
+ *
37
+ * NOTE: what may be PUBLISHED together is a separate limit — a carousel is at
38
+ * most ten anywhere. This governs uploading only.
39
+ */
40
+ export class AgentUploadBatchRequestDTO {
41
+ files: AgentUploadFileRequestDTO[];
42
+
43
+ constructor(data: AgentUploadBatchRequestDTO) {
44
+ this.files = data.files;
45
+ }
46
+ }
47
+
48
+ // ── Responses ────────────────────────────────────────────────────────────────
49
+
50
+ /** Why one file was refused. The rest of the batch is unaffected (FR-015). */
51
+ export type T_AgentUploadRejectionReason =
52
+ | 'file_too_large'
53
+ | 'unsupported_type'
54
+ | 'invalid_name'
55
+ | 'invalid_size';
56
+
57
+ /** One accepted file: where to put the bytes, and how to refer to it afterwards. */
58
+ export class AgentUploadTargetDTO {
59
+ fileName: string;
60
+ /**
61
+ * The opaque handle used when composing a post, as `media:<id>`.
62
+ * Neither the creator nor the model ever handles a raw storage location.
63
+ */
64
+ mediaRef: string;
65
+ /**
66
+ * Single-purpose, short-lived write authority for THIS file only.
67
+ * Not general storage access, not account access.
68
+ */
69
+ uploadUrl: string;
70
+ /** Seconds. */
71
+ expiresIn: number;
72
+
73
+ constructor(data: AgentUploadTargetDTO) {
74
+ this.fileName = data.fileName;
75
+ this.mediaRef = data.mediaRef;
76
+ this.uploadUrl = data.uploadUrl;
77
+ this.expiresIn = data.expiresIn;
78
+ }
79
+ }
80
+
81
+ /** One refused file, named so the creator knows which (FR-015). */
82
+ export class AgentUploadRejectionDTO {
83
+ fileName: string;
84
+ reason: T_AgentUploadRejectionReason;
85
+ /** Plain language, for the creator. No status codes, no internal rule names. */
86
+ message: string;
87
+
88
+ constructor(data: AgentUploadRejectionDTO) {
89
+ this.fileName = data.fileName;
90
+ this.reason = data.reason;
91
+ this.message = data.message;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * A batch result. Per-file outcomes, never a single aggregate verdict.
97
+ *
98
+ * `nextCursor` exists because a large batch's response has to fit in one tool
99
+ * result. The 77-file case is measured rather than assumed (spec FR-030,
100
+ * review finding F4); when a flat list does not fit, entries are paged.
101
+ */
102
+ export class AgentUploadBatchResponseDTO {
103
+ targets: AgentUploadTargetDTO[];
104
+ rejections: AgentUploadRejectionDTO[];
105
+ /** Present when more entries remain; pass back to fetch the next page. */
106
+ nextCursor?: string | null;
107
+
108
+ constructor(data: AgentUploadBatchResponseDTO) {
109
+ this.targets = data.targets;
110
+ this.rejections = data.rejections;
111
+ this.nextCursor = data.nextCursor ?? null;
112
+ }
113
+ }
114
+
115
+ // ── Confirmation ─────────────────────────────────────────────────────────────
116
+
117
+ /**
118
+ * Per-ref confirmation that the bytes actually landed.
119
+ *
120
+ * `ready` means VERIFIED PRESENT IN STORAGE, server-side — never "the host said
121
+ * so" (spec FR-012). Media that is not ready cannot be attached to a post
122
+ * (FR-017), which is what keeps a failed upload from becoming a published post
123
+ * with missing images.
124
+ */
125
+ export class AgentUploadConfirmationDTO {
126
+ mediaRef: string;
127
+ ready: boolean;
128
+ /** Set when not ready, in plain language the creator can act on. */
129
+ message?: string;
130
+
131
+ constructor(data: AgentUploadConfirmationDTO) {
132
+ this.mediaRef = data.mediaRef;
133
+ this.ready = data.ready;
134
+ this.message = data.message;
135
+ }
136
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Agent domain — barrel export (spec 163).
3
+ *
4
+ * Available from 'ad2app-lib/types' via:
5
+ * import { AgentGrantDTO, AGENT_SCOPES, ... } from 'ad2app-lib/types';
6
+ */
7
+
8
+ export * from './I_AgentGrant';
9
+ export * from './I_AgentMediaUpload';
@@ -37,5 +37,8 @@ export * from "./I_SM_Platform";
37
37
  // ── Scheduling domain ─────────────────────────────────────────────────────────
38
38
  export * from "./scheduling";
39
39
 
40
+ // ── Agent domain (delegated authority + direct media upload, spec 163) ───────
41
+ export * from "./agent";
42
+
40
43
  // ── Access control ────────────────────────────────────────────────────────────
41
44
  export * from "./I_AccessDenial";