ad2app-lib 1.38.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.
@@ -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.38.0",
3
+ "version": "1.39.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "commonjs",
@@ -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";