@company-semantics/contracts 39.8.0 → 41.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@company-semantics/contracts",
3
- "version": "39.8.0",
3
+ "version": "41.0.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
2
- export const SPEC_HASH = 'e8cf36a33d43' as const;
3
- export const SPEC_HASH_FULL = 'e8cf36a33d43bec5e4383c8edbfbdccd37fe8d081a5ddd02e96c83cb5e7bd21a' as const;
2
+ export const SPEC_HASH = 'c0f32d11bf0f' as const;
3
+ export const SPEC_HASH_FULL = 'c0f32d11bf0fdc59ee0a7bf84bee984f2f8109838d2659b6e66709e0198dcd4e' as const;
@@ -1423,23 +1423,6 @@ export interface paths {
1423
1423
  patch?: never;
1424
1424
  trace?: never;
1425
1425
  };
1426
- "/api/company-md/docs/{id}/content": {
1427
- parameters: {
1428
- query?: never;
1429
- header?: never;
1430
- path?: never;
1431
- cookie?: never;
1432
- };
1433
- get?: never;
1434
- /** Update a company.md document content */
1435
- put: operations["updateCompanyMdDocContent"];
1436
- post?: never;
1437
- delete?: never;
1438
- options?: never;
1439
- head?: never;
1440
- patch?: never;
1441
- trace?: never;
1442
- };
1443
1426
  "/api/company-md/docs/{id}/title": {
1444
1427
  parameters: {
1445
1428
  query?: never;
@@ -4620,6 +4603,11 @@ export interface components {
4620
4603
  /** @enum {string} */
4621
4604
  accessState?: "full" | "redacted_admin" | "locked_requestable";
4622
4605
  canRequestAccess?: boolean;
4606
+ pendingAccessRequest?: {
4607
+ id: string;
4608
+ requestedAccessLevel: ("editor" | "commenter" | "viewer") | null;
4609
+ createdAt: string;
4610
+ } | null;
4623
4611
  /** @enum {string} */
4624
4612
  visibility: "private" | "unit" | "org";
4625
4613
  /** @enum {string} */
@@ -4660,9 +4648,6 @@ export interface components {
4660
4648
  createdAt: string;
4661
4649
  updatedAt: string;
4662
4650
  };
4663
- UpdateCompanyMdContentRequest: {
4664
- content: string;
4665
- };
4666
4651
  UpdateCompanyMdTitleRequest: {
4667
4652
  title: string;
4668
4653
  };
@@ -4746,9 +4731,12 @@ export interface components {
4746
4731
  ApproveAccessRequest: {
4747
4732
  /** @enum {string} */
4748
4733
  accessLevel: "editor" | "commenter" | "viewer";
4734
+ notify?: boolean;
4735
+ message?: string;
4749
4736
  };
4750
4737
  DenyAccessRequest: {
4751
4738
  reason?: string;
4739
+ notify?: boolean;
4752
4740
  };
4753
4741
  /** @description Org-wide context-doc discovery hits the caller may view. */
4754
4742
  SearchContextDocsResponse: {
@@ -8224,30 +8212,6 @@ export interface operations {
8224
8212
  };
8225
8213
  };
8226
8214
  };
8227
- updateCompanyMdDocContent: {
8228
- parameters: {
8229
- query?: never;
8230
- header?: never;
8231
- path: {
8232
- id: string;
8233
- };
8234
- cookie?: never;
8235
- };
8236
- requestBody: {
8237
- content: {
8238
- "application/json": components["schemas"]["UpdateCompanyMdContentRequest"];
8239
- };
8240
- };
8241
- responses: {
8242
- /** @description Content updated */
8243
- 204: {
8244
- headers: {
8245
- [name: string]: unknown;
8246
- };
8247
- content?: never;
8248
- };
8249
- };
8250
- };
8251
8215
  updateCompanyMdDocTitle: {
8252
8216
  parameters: {
8253
8217
  query?: never;
@@ -12,6 +12,7 @@
12
12
  import { z } from "zod";
13
13
  import { CursorPageSchema } from "../api/primitives";
14
14
  import { EffectiveAccessResponseSchema } from "../permissions/share-api";
15
+ import { ViewerPendingAccessRequestSchema } from "../permissions/access-request";
15
16
 
16
17
  // ---------------------------------------------------------------------------
17
18
  // Company.md Sub-schemas
@@ -112,6 +113,15 @@ export const CompanyMdDocResponseSchema = z.object({
112
113
  .optional(),
113
114
  /** Whether the requesting actor may request access (ADR-BE-348 / ADR-BE-338). */
114
115
  canRequestAccess: z.boolean().optional(),
116
+ /**
117
+ * The requesting actor's own OPEN request on this node, if any
118
+ * (ADR-CONTRACTS-106). `canRequestAccess` answers "may you ask?" and is
119
+ * derived from discoverability + read-state alone, so it stays true after you
120
+ * have asked — this field is what distinguishes "you may ask" from "you
121
+ * already did", and it is the only thing standing between the locked view and
122
+ * an actor re-submitting a request they already filed.
123
+ */
124
+ pendingAccessRequest: ViewerPendingAccessRequestSchema.nullable().optional(),
115
125
  visibility: z.enum(["private", "unit", "org"]),
116
126
  // Governance + discovery projections (ADR-BE-348 / ADR-CTRL-194). Present on
117
127
  // the TS contract since the projection split; now promoted into the published
@@ -34,7 +34,6 @@ export const openApiRoutes = {
34
34
  '/api/company-md/docs/{id}/collab/stream': ['GET'],
35
35
  '/api/company-md/docs/{id}/collab/sync': ['GET'],
36
36
  '/api/company-md/docs/{id}/collab/updates': ['POST'],
37
- '/api/company-md/docs/{id}/content': ['PUT'],
38
37
  '/api/company-md/docs/{id}/context-bank': ['GET'],
39
38
  '/api/company-md/docs/{id}/context-bank/associate': ['POST'],
40
39
  '/api/company-md/docs/{id}/context-bank/retry': ['POST'],
@@ -11,6 +11,7 @@ exports[`NOTIFICATION_DEFINITIONS > titles every kind 1`] = `
11
11
  "chat.shared · No preview": "A chat has been shared with you",
12
12
  "chat.shared · Short": "A chat has been shared with you",
13
13
  "companyMd.access_request_approved · Editor": "Your access request was approved",
14
+ "companyMd.access_request_approved · Editor with message": "Your access request was approved",
14
15
  "companyMd.access_request_approved · Viewer": "Your access request was approved",
15
16
  "companyMd.access_request_denied · No reason": "Your access request was reviewed",
16
17
  "companyMd.access_request_denied · With reason": "Your access request was reviewed",
@@ -1603,6 +1603,185 @@ This notification was sent via Company Semantics.
1603
1603
  Access is now active.
1604
1604
 
1605
1605
 
1606
+ /* EOM */
1607
+ ⓒ 2026 • Company Semantics
1608
+ https://companysemantics.ai
1609
+ ",
1610
+ }
1611
+ `;
1612
+
1613
+ exports[`email render > companyMd.access_request_approved · Editor with message 1`] = `
1614
+ {
1615
+ "amp": "<!DOCTYPE html>
1616
+ <html amp4email data-css-strict lang="en">
1617
+ <head><meta charset="utf-8">
1618
+ <script async src="https://cdn.ampproject.org/v0.js"></script>
1619
+ <style amp4email-boilerplate>body{visibility:hidden}</style>
1620
+ <style amp-custom>.csr-body { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; color: #1a1a1a; background-color: #ffffff; margin: 0; padding: 0; }
1621
+ .csr-frame { max-width: 520px; margin: 0 auto; }
1622
+ .csr-p-normal { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0 0 20px 0; }
1623
+ .csr-p-tight { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0 0 4px 0; }
1624
+ .csr-p-none { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0; }
1625
+ .csr-eom { color: #bbb; }
1626
+ .csr-signature-link { color: #0047FF; text-decoration: none; }
1627
+ .csr-warning { color: #e7000b; }
1628
+ .csr-hr-normal { border: none; border-top: 1px solid #bbb; margin: 0 0 20px 0; }
1629
+ .csr-hr-tight { border: none; border-top: 1px solid #bbb; margin: 0 0 4px 0; }
1630
+ .csr-hr-none { border: none; border-top: 1px solid #bbb; margin: 0; }
1631
+ .csr-hero-normal { max-width: 100%; margin: 0 0 20px 0; }
1632
+ .csr-hero-tight { max-width: 100%; margin: 0 0 4px 0; }
1633
+ .csr-hero-none { max-width: 100%; margin: 0; }
1634
+ .csr-cta-box-normal { display: inline-block; border: 1px solid #0047FF; border-radius: 2px; margin: 0 0 20px 0; max-width: 220px; }
1635
+ .csr-cta-box-tight { display: inline-block; border: 1px solid #0047FF; border-radius: 2px; margin: 0 0 4px 0; max-width: 220px; }
1636
+ .csr-cta-box-none { display: inline-block; border: 1px solid #0047FF; border-radius: 2px; margin: 0; max-width: 220px; }
1637
+ .csr-cta-cell-linked { text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; }
1638
+ .csr-cta-cell-code { padding: 16px 24px; text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; }
1639
+ .csr-cta-label { text-decoration: underline; }
1640
+ .csr-cta-anchor { display: block; padding: 16px 24px; color: #0047FF; text-decoration: none; }
1641
+ .csr-cta-code { color: #0047FF; }
1642
+ .csr-chat-rule-top { border: none; border-top: 1px solid #bbb; margin: 12px 0 24px 0; }
1643
+ .csr-chat-rule-bottom { border: none; border-top: 1px solid #bbb; margin: 24px 0 12px 0; }
1644
+ .csr-chat-row-24 { margin: 0 0 24px 0; }
1645
+ .csr-chat-row-16 { margin: 0 0 16px 0; }
1646
+ .csr-chat-attribution { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; color: #666; text-align: right; padding-top: 6px; padding-right: 1ch; }
1647
+ .csr-chat-avatar-left { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-right: 8px; vertical-align: bottom; white-space: nowrap; }
1648
+ .csr-chat-avatar-left-hidden { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-right: 8px; vertical-align: bottom; visibility: hidden; white-space: nowrap; }
1649
+ .csr-chat-avatar-right { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-left: 8px; vertical-align: bottom; white-space: nowrap; }
1650
+ .csr-chat-avatar-right-hidden { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-left: 8px; vertical-align: bottom; visibility: hidden; white-space: nowrap; }
1651
+ .csr-chat-channel-left { width: 100%; text-align: left; }
1652
+ .csr-chat-channel-right { width: 100%; text-align: right; }
1653
+ .csr-chat-bubble-wrap { display: inline-block; max-width: 100%; vertical-align: bottom; }
1654
+ .csr-bubble-user { border-radius: 8px 8px 0 8px; padding: 10px 14px; text-align: right; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; color: #ffffff; background: #666; }
1655
+ .csr-bubble-assistant { border-radius: 8px 8px 8px 0; padding: 10px 14px; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; color: #ffffff; background: #666; }
1656
+ .csr-dots-over-cta { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 20px; font-weight: bold; color: #666; text-align: center; margin: 0 0 16px 0; }
1657
+ .csr-dots-cell { width: 100%; text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 20px; font-weight: bold; color: #666; }
1658
+ .csr-cta-stack { display: inline-block; text-align: left; }
1659
+ .cs-cta-hover:hover { background-color: #d1ffff; }</style></head>
1660
+ <body class="csr-body">
1661
+ <div class="csr-frame">
1662
+ <p class="csr-p-normal">Hi there,</p>
1663
+ <p class="csr-p-normal">Your access request was approved.</p>
1664
+ <hr class="cs-faint csr-chat-rule-top">
1665
+ <table cellpadding="0" cellspacing="0" border="0" width="100%" class="csr-chat-row-16">
1666
+ <tr>
1667
+ <td class="csr-chat-avatar-left-hidden">[c_S]</td>
1668
+ <td class="csr-chat-channel-right"><table cellpadding="0" cellspacing="0" border="0" class="csr-chat-bubble-wrap">
1669
+ <tr><td class="cs-bubble csr-bubble-user">Welcome aboard — start with the onboarding section.</td></tr>
1670
+ </table></td>
1671
+ <td class="csr-chat-avatar-right">(•̀_ರ╮)</td>
1672
+ </tr>
1673
+ <tr>
1674
+ <td></td>
1675
+ <td class="cs-meta csr-chat-attribution">Jordan Lee</td>
1676
+ <td></td>
1677
+ </tr>
1678
+ </table>
1679
+ <table cellpadding="0" cellspacing="0" border="0" width="100%" class="csr-chat-row-24">
1680
+ <tr>
1681
+ <td class="csr-chat-avatar-left-hidden">[c_S]</td>
1682
+ <td class="csr-chat-channel-right"><div class="csr-cta-stack"><table cellpadding="0" cellspacing="0" border="0" class="cs-cta cs-cta-hover csr-cta-box-none">
1683
+ <tr><td class="csr-cta-cell-linked">
1684
+ <a href="https://app.companysemantics.ai/doc/handbook" class="cs-link csr-cta-anchor">&gt;&gt; <span class="csr-cta-label">OPEN</span> &lt;&lt;</a>
1685
+ </td></tr>
1686
+ </table></div></td>
1687
+ <td class="csr-chat-avatar-right-hidden">(•̀_ರ╮)</td>
1688
+ </tr>
1689
+ </table>
1690
+ <hr class="cs-faint csr-chat-rule-bottom">
1691
+ <p class="csr-p-tight">Document: &quot;Engineering Handbook&quot;</p>
1692
+ <p class="csr-p-tight">Access: can edit</p>
1693
+ <p class="csr-p-normal">Approved by: Jordan Lee</p>
1694
+ <p class="csr-p-none">This notification was sent via Company Semantics.<br>Access is now active.</p>
1695
+ <p class="csr-p-none"><br><br><span class="cs-faint csr-eom">/* EOM */</span><br>ⓒ 2026 • Company Semantics<br><a href="https://companysemantics.ai" target="_blank" rel="noopener noreferrer" class="cs-link csr-signature-link">https://companysemantics.ai</a></p>
1696
+ </div>
1697
+ </body>
1698
+ </html>",
1699
+ "html": "<!DOCTYPE html>
1700
+ <html lang="en">
1701
+ <head><meta charset="UTF-8">
1702
+ <meta name="color-scheme" content="light dark">
1703
+ <meta name="supported-color-schemes" content="light dark">
1704
+ <style>.cs-cta-hover:hover { background-color: #d1ffff !important; }
1705
+ @media (prefers-color-scheme: dark) {
1706
+ body { background-color: #1a1a1a !important; color: #e8e8e8 !important; }
1707
+ .cs-faint { color: #666 !important; border-top-color: #666 !important; }
1708
+ .cs-meta { color: #8a8a8a !important; }
1709
+ .cs-cta { border-color: #00ffff !important; }
1710
+ .cs-cta-hover:hover { background-color: #154848 !important; }
1711
+ .cs-bubble { background: #154848 !important; color: #ffffff !important; }
1712
+ .cs-dots { color: #8a8a8a !important; }
1713
+ .cs-link { color: #00ffff !important; }
1714
+ .cs-destructive { color: #ff6b6b !important; }
1715
+ }</style>
1716
+ <!--[if mso]><style>.cs-cta td { padding: 16px 24px !important; }
1717
+ .cs-cta a { padding: 0 !important; }</style><![endif]--></head>
1718
+ <body style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; color: #1a1a1a; background-color: #ffffff; margin: 0; padding: 0;">
1719
+ <div style="max-width: 520px; margin: 0 auto;">
1720
+ <p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0 0 20px 0;">Hi there,</p>
1721
+ <p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0 0 20px 0;">Your access request was approved.</p>
1722
+ <hr class="cs-faint" style="border: none; border-top: 1px solid #bbb; margin: 12px 0 24px 0;">
1723
+ <table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin: 0 0 16px 0;">
1724
+ <tr>
1725
+ <td style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-right: 8px; vertical-align: bottom; visibility: hidden; white-space: nowrap;">[c_S]</td>
1726
+ <td style="width: 100%; text-align: right;"><table cellpadding="0" cellspacing="0" border="0" style="display: inline-block; max-width: 100%; vertical-align: bottom;">
1727
+ <tr><td class="cs-bubble" style="border-radius: 8px 8px 0 8px; padding: 10px 14px; text-align: right; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; color: #ffffff; background: #666;">Welcome aboard — start with the onboarding section.</td></tr>
1728
+ </table></td>
1729
+ <td style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-left: 8px; vertical-align: bottom; white-space: nowrap;">(•̀_ರ╮)</td>
1730
+ </tr>
1731
+ <tr>
1732
+ <td></td>
1733
+ <td class="cs-meta" style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; color: #666; text-align: right; padding-top: 6px; padding-right: 1ch;">Jordan Lee</td>
1734
+ <td></td>
1735
+ </tr>
1736
+ </table>
1737
+ <table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin: 0 0 24px 0;">
1738
+ <tr>
1739
+ <td style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-right: 8px; vertical-align: bottom; visibility: hidden; white-space: nowrap;">[c_S]</td>
1740
+ <td style="width: 100%; text-align: right;"><div style="display: inline-block; text-align: left;"><table cellpadding="0" cellspacing="0" border="0" class="cs-cta cs-cta-hover" style="display: inline-block; border: 1px solid #0047FF; border-radius: 2px; margin: 0; max-width: 220px;">
1741
+ <tr><td style="text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px;">
1742
+ <a href="https://app.companysemantics.ai/doc/handbook" class="cs-link" style="display: block; padding: 16px 24px; color: #0047FF; text-decoration: none;">&gt;&gt; <span style="text-decoration: underline;">OPEN</span> &lt;&lt;</a>
1743
+ </td></tr>
1744
+ </table></div></td>
1745
+ <td style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-left: 8px; vertical-align: bottom; visibility: hidden; white-space: nowrap;">(•̀_ರ╮)</td>
1746
+ </tr>
1747
+ </table>
1748
+ <hr class="cs-faint" style="border: none; border-top: 1px solid #bbb; margin: 24px 0 12px 0;">
1749
+ <p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0 0 4px 0;">Document: &quot;Engineering Handbook&quot;</p>
1750
+ <p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0 0 4px 0;">Access: can edit</p>
1751
+ <p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0 0 20px 0;">Approved by: Jordan Lee</p>
1752
+ <p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0;">This notification was sent via Company Semantics.<br>Access is now active.</p>
1753
+ <p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0;"><br><br><span class="cs-faint" style="color: #bbb;">/* EOM */</span><br>ⓒ 2026 • Company Semantics<br><a href="https://companysemantics.ai" target="_blank" rel="noopener noreferrer" class="cs-link" style="color: #0047FF; text-decoration: none;">https://companysemantics.ai</a></p>
1754
+ </div>
1755
+ </body>
1756
+ </html>",
1757
+ "subject": "Your access request was approved",
1758
+ "text": "Hi there,
1759
+
1760
+ Your access request was approved.
1761
+
1762
+ ________________________________________________________
1763
+
1764
+ ┌──────────────────────────────────────┐
1765
+ │ Welcome aboard — start with the │
1766
+ │ onboarding section. │ (•̀_ರ╮)
1767
+ └──────────────────────────────────────┘
1768
+ Jordan Lee
1769
+
1770
+ *----------------*
1771
+ | >> OPEN << |
1772
+ *----------------*
1773
+
1774
+ https://app.companysemantics.ai/doc/handbook
1775
+ ________________________________________________________
1776
+
1777
+ Document: "Engineering Handbook"
1778
+ Access: can edit
1779
+ Approved by: Jordan Lee
1780
+
1781
+ This notification was sent via Company Semantics.
1782
+ Access is now active.
1783
+
1784
+
1606
1785
  /* EOM */
1607
1786
  ⓒ 2026 • Company Semantics
1608
1787
  https://companysemantics.ai
@@ -212,6 +212,15 @@ add("companyMd.access_request_approved", "Editor", {
212
212
  accessLevel: "editor",
213
213
  docUrl: `${APP}/doc/handbook`,
214
214
  });
215
+ // The approver's note replaces the "Open to view." prompt rather than stacking
216
+ // on it — the branch this variant exists to pin.
217
+ add("companyMd.access_request_approved", "Editor with message", {
218
+ approverName: "Jordan Lee",
219
+ docTitle: "Engineering Handbook",
220
+ accessLevel: "editor",
221
+ docUrl: `${APP}/doc/handbook`,
222
+ message: "Welcome aboard — start with the onboarding section.",
223
+ });
215
224
  add("companyMd.access_request_approved", "Viewer", {
216
225
  approverName: "Jordan Lee",
217
226
  docTitle: "Engineering Handbook",
@@ -212,6 +212,15 @@ add("companyMd.access_request_approved", "Editor", {
212
212
  accessLevel: "editor",
213
213
  docUrl: `${APP}/doc/handbook`,
214
214
  });
215
+ // The approver's note replaces the "Open to view." prompt rather than stacking
216
+ // on it — the branch this variant exists to pin, across every channel.
217
+ add("companyMd.access_request_approved", "Editor with message", {
218
+ approverName: "Jordan Lee",
219
+ docTitle: "Engineering Handbook",
220
+ accessLevel: "editor",
221
+ docUrl: `${APP}/doc/handbook`,
222
+ message: "Welcome aboard — start with the onboarding section.",
223
+ });
215
224
  add("companyMd.access_request_approved", "Viewer", {
216
225
  approverName: "Jordan Lee",
217
226
  docTitle: "Engineering Handbook",
@@ -1,5 +1,12 @@
1
1
  /**
2
2
  * Company.md access-approved notification (sent to the requester).
3
+ *
4
+ * The chat unit carries EITHER the approver's own words or the bare "Open to
5
+ * view." prompt — never both. An approver who wrote a note has already said what
6
+ * the prompt would say, and stacking the two reads as a form letter with a
7
+ * comment bolted on. Same shape as `share.granted` (ADR-CONTRACTS-106); the call
8
+ * to action stays in place either way, since approval always resolves to a doc
9
+ * the requester can now open.
3
10
  */
4
11
 
5
12
  import type { NotificationDefinition } from "../definition";
@@ -10,7 +17,7 @@ export const accessApprovedDefinition: NotificationDefinition<"companyMd.access_
10
17
  {
11
18
  kind: "companyMd.access_request_approved",
12
19
  compose: (payload, context) => {
13
- const { approverName, docTitle, accessLevel, docUrl } = payload;
20
+ const { approverName, docTitle, accessLevel, docUrl, message } = payload;
14
21
 
15
22
  return {
16
23
  metadata: {
@@ -25,11 +32,18 @@ export const accessApprovedDefinition: NotificationDefinition<"companyMd.access_
25
32
  {
26
33
  type: "chatUnit",
27
34
  items: [
28
- {
29
- type: "message",
30
- role: "assistant",
31
- text: "Open to view.",
32
- },
35
+ message
36
+ ? {
37
+ type: "message",
38
+ role: "user",
39
+ text: message,
40
+ from: approverName,
41
+ }
42
+ : {
43
+ type: "message",
44
+ role: "assistant",
45
+ text: "Open to view.",
46
+ },
33
47
  { type: "callToAction", label: "OPEN", href: docUrl },
34
48
  ],
35
49
  },
@@ -28,8 +28,13 @@ export const accessRequestedDefinition: NotificationDefinition<"companyMd.access
28
28
  {
29
29
  kind: "companyMd.access_requested",
30
30
  compose: (payload, context) => {
31
- const { requesterName, docTitle, message, requestedAccessLevel, reviewUrl } =
32
- payload;
31
+ const {
32
+ requesterName,
33
+ docTitle,
34
+ message,
35
+ requestedAccessLevel,
36
+ reviewUrl,
37
+ } = payload;
33
38
 
34
39
  return {
35
40
  metadata: {
@@ -151,6 +151,12 @@ export interface NotificationPayloads {
151
151
  accessLevel: "editor" | "commenter" | "viewer";
152
152
  /** Full URL to view the node */
153
153
  docUrl: string;
154
+ /**
155
+ * Optional note from the approver, included in the notification only (not
156
+ * persisted) — the approval counterpart of the deny kind's `reason`
157
+ * (ADR-CONTRACTS-106).
158
+ */
159
+ message?: string;
154
160
  };
155
161
  /** Sent to the requester when an owner denies (ADR-BE-338). */
156
162
  "companyMd.access_request_denied": {
@@ -6,6 +6,8 @@
6
6
  * @see PRD-00173 for design rationale
7
7
  */
8
8
 
9
+ import type { ViewerPendingAccessRequest } from "../permissions/access-request";
10
+
9
11
  /**
10
12
  * Visibility band for a Company.md document — the canonical AUTH
11
13
  * `entity_visibility` vocabulary (ADR-CONT-049, backend ADR-BE-178).
@@ -218,6 +220,13 @@ export interface CompanyMdDocCore extends CompanyMdNodeIdentity {
218
220
  * recipient to an `owners` node may still be allowed to request).
219
221
  */
220
222
  readonly canRequestAccess?: boolean;
223
+ /**
224
+ * The requesting actor's own OPEN request on this node (ADR-CONTRACTS-106).
225
+ * `canRequestAccess` is derived from discoverability + read-state and stays
226
+ * true once you have asked, so this is the field that separates "you may ask"
227
+ * from "you already did". Absent/`null` ⇒ nothing open.
228
+ */
229
+ readonly pendingAccessRequest?: ViewerPendingAccessRequest | null;
221
230
  /**
222
231
  * Whether this doc is served by the real-time collaboration protocol
223
232
  * (ADR-CONT-102). When `true` the client may open `/collab/sync` + the collab
@@ -24,10 +24,38 @@ export const ACCESS_REQUEST_STATUSES = [
24
24
  export const AccessRequestStatusSchema = z.enum(ACCESS_REQUEST_STATUSES);
25
25
  export type AccessRequestStatus = z.infer<typeof AccessRequestStatusSchema>;
26
26
 
27
+ /**
28
+ * Character cap on an access-request message.
29
+ *
30
+ * A note to an owner, not a document: 280 is enough to say why you want access
31
+ * and short enough to read in an inbox row. Exported so the request dialog's
32
+ * `maxLength` and its remaining-character counter read the SAME number the API
33
+ * rejects on — a UI that let you type past the server's limit would fail the
34
+ * send with no explanation.
35
+ *
36
+ * Enforced in three places, deliberately: the input caps typing, this schema
37
+ * rejects the request, and a CHECK constraint on
38
+ * `company_md_doc_access_requests.message` bounds anything that reaches the
39
+ * table by another path.
40
+ */
41
+ export const ACCESS_REQUEST_MESSAGE_MAX_LENGTH = 280;
42
+
43
+ /**
44
+ * Plain text only. Rejects C0/C1 control characters — a pasted binary blob or a
45
+ * terminal dump — while allowing the newlines and tabs a human types. The field
46
+ * is rendered as text, never as markup, so this is about what may be STORED,
47
+ * not about escaping at render time.
48
+ */
49
+ const PLAIN_TEXT = /^[^\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]*$/;
50
+
27
51
  /** POST body to create an access request. */
28
52
  export const AccessRequestCreateSchema = z.object({
29
53
  /** Optional context the requester sends to the owner(s). */
30
- message: z.string().max(2000).optional(),
54
+ message: z
55
+ .string()
56
+ .max(ACCESS_REQUEST_MESSAGE_MAX_LENGTH)
57
+ .regex(PLAIN_TEXT, "Message must be plain text")
58
+ .optional(),
31
59
  /**
32
60
  * The band the requester is asking for (ADR-CONTRACTS-098 / ADR-BE-454):
33
61
  * `viewer` from the locked preview, `editor` from the view-only Editor tab
@@ -65,18 +93,89 @@ export const AccessRequestSchema = z.object({
65
93
  });
66
94
  export type AccessRequest = z.infer<typeof AccessRequestSchema>;
67
95
 
96
+ /**
97
+ * The requesting actor's OWN open request on a node, surfaced on the single-doc
98
+ * read so a locked view can state that they already asked instead of offering
99
+ * the ask a second time (ADR-CONTRACTS-106).
100
+ *
101
+ * A deliberately NARROWER projection than the owner inbox's
102
+ * {@link AccessRequestSchema}: a requester is not authorized to learn who else
103
+ * asked or how anything was resolved, and needs only what they asked for and
104
+ * when. Absent/`null` ⇒ no open request, which is what an actor who never asked,
105
+ * and an actor whose request was already resolved, both see.
106
+ *
107
+ * Single-doc read ONLY — deliberately not on the tree node schema, where a
108
+ * per-node pending lookup would be an N+1 across the whole tree.
109
+ */
110
+ export const ViewerPendingAccessRequestSchema = z.object({
111
+ id: z.string(),
112
+ /**
113
+ * The band they asked for. Null for requests filed before the field existed
114
+ * or by a client that named none.
115
+ */
116
+ requestedAccessLevel: GrantableAccessLevelSchema.nullable(),
117
+ createdAt: z.string(),
118
+ });
119
+ export type ViewerPendingAccessRequest = z.infer<
120
+ typeof ViewerPendingAccessRequestSchema
121
+ >;
122
+
123
+ /**
124
+ * Character cap on the approver's note to the requester.
125
+ *
126
+ * Deliberately the 2000 the deny `reason` already used, not the requester's 280:
127
+ * the requester writes a one-line ask into an owner's inbox row, the approver
128
+ * writes a reply that is read on its own in an email body.
129
+ */
130
+ export const ACCESS_DECISION_MESSAGE_MAX_LENGTH = 2000;
131
+
132
+ /**
133
+ * Whether the requester is told about the decision.
134
+ *
135
+ * ABSENT MEANS SEND (ADR-CONTRACTS-106). Approve/deny have always emailed the
136
+ * requester unconditionally, so a client that omits the field — an older app, a
137
+ * script — must keep getting that behavior. Only an explicit `false` suppresses
138
+ * the notification; a default of `false` here would silence every existing
139
+ * caller.
140
+ */
141
+ const NotifySchema = z.boolean().optional();
142
+
68
143
  /**
69
144
  * POST body to approve a request. The owner chooses the granted level (default
70
145
  * `viewer` is applied server-side if the field is omitted by older clients).
71
146
  */
72
147
  export const ApproveAccessRequestSchema = z.object({
73
148
  accessLevel: GrantableAccessLevelSchema,
149
+ /** Whether to email the requester. Absent ⇒ send (see {@link NotifySchema}). */
150
+ notify: NotifySchema,
151
+ /**
152
+ * Optional note from the approver, quoted to the requester in the approval
153
+ * notification only — never persisted on the request or the minted grant.
154
+ * Ignored when `notify` is false: there is no other surface that would show
155
+ * it. Mirrors `share.granted`'s granter message.
156
+ */
157
+ message: z
158
+ .string()
159
+ .max(ACCESS_DECISION_MESSAGE_MAX_LENGTH)
160
+ .regex(PLAIN_TEXT, "Message must be plain text")
161
+ .optional(),
74
162
  });
75
163
  export type ApproveAccessRequest = z.infer<typeof ApproveAccessRequestSchema>;
76
164
 
77
165
  /** POST body to deny a request. */
78
166
  export const DenyAccessRequestSchema = z.object({
79
- /** Optional reason, surfaced to the requester (email only). */
80
- reason: z.string().max(2000).optional(),
167
+ /**
168
+ * Optional reason, surfaced to the requester (email only). This is the deny
169
+ * verb's message carrier — the approver's composer writes here rather than to
170
+ * a second `message` field, because the denied notification already quotes
171
+ * `reason` and a doc's denial has no other content.
172
+ */
173
+ reason: z
174
+ .string()
175
+ .max(ACCESS_DECISION_MESSAGE_MAX_LENGTH)
176
+ .regex(PLAIN_TEXT, "Reason must be plain text")
177
+ .optional(),
178
+ /** Whether to email the requester. Absent ⇒ send (see {@link NotifySchema}). */
179
+ notify: NotifySchema,
81
180
  });
82
181
  export type DenyAccessRequest = z.infer<typeof DenyAccessRequestSchema>;