@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 +1 -1
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +8 -44
- package/src/content/schemas.ts +10 -0
- package/src/generated/openapi-routes.ts +0 -1
- package/src/notifications/__tests__/__snapshots__/registry.test.ts.snap +1 -0
- package/src/notifications/__tests__/__snapshots__/render-snapshot.test.ts.snap +179 -0
- package/src/notifications/__tests__/fixtures.ts +9 -0
- package/src/notifications/__tests__/render-snapshot.test.ts +9 -0
- package/src/notifications/kinds/company-md-access-approved.ts +20 -6
- package/src/notifications/kinds/company-md-access-requested.ts +7 -2
- package/src/notifications/payloads.ts +6 -0
- package/src/org/company-md.ts +9 -0
- package/src/permissions/access-request.ts +102 -3
package/package.json
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
|
|
2
|
-
export const SPEC_HASH = '
|
|
3
|
-
export const SPEC_HASH_FULL = '
|
|
2
|
+
export const SPEC_HASH = 'c0f32d11bf0f' as const;
|
|
3
|
+
export const SPEC_HASH_FULL = 'c0f32d11bf0fdc59ee0a7bf84bee984f2f8109838d2659b6e66709e0198dcd4e' as const;
|
package/src/api/generated.ts
CHANGED
|
@@ -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;
|
package/src/content/schemas.ts
CHANGED
|
@@ -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">>> <span class="csr-cta-label">OPEN</span> <<</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: "Engineering Handbook"</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;">>> <span style="text-decoration: underline;">OPEN</span> <<</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: "Engineering Handbook"</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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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 {
|
|
32
|
-
|
|
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": {
|
package/src/org/company-md.ts
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
80
|
-
|
|
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>;
|