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.
- package/dist/analytics/index.d.ts +44 -3
- package/dist/analytics/index.js +42 -1
- package/dist/types/agent/I_AgentGrant.d.ts +99 -0
- package/dist/types/agent/I_AgentGrant.js +109 -0
- package/dist/types/agent/I_AgentMediaUpload.d.ts +90 -0
- package/dist/types/agent/I_AgentMediaUpload.js +92 -0
- package/dist/types/agent/index.d.ts +8 -0
- package/dist/types/agent/index.js +24 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +2 -0
- package/package.json +1 -1
- package/src/analytics/account-connect-blocked.test.ts +7 -1
- package/src/analytics/index.test.ts +15 -0
- package/src/analytics/index.ts +77 -2
- package/src/types/agent/I_AgentGrant.ts +161 -0
- package/src/types/agent/I_AgentMediaUpload.ts +136 -0
- package/src/types/agent/index.ts +9 -0
- package/src/types/index.ts +3 -0
|
@@ -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
|
};
|
package/dist/analytics/index.js
CHANGED
|
@@ -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,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);
|
package/dist/types/index.d.ts
CHANGED
package/dist/types/index.js
CHANGED
|
@@ -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
|
@@ -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
|
|
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)", () => {
|
package/src/analytics/index.ts
CHANGED
|
@@ -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'
|
|
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
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -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";
|