@anthusai/papyrus 1.4.0-next.5 → 1.4.0-next.6
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/amplify/data/resource.js +10 -0
- package/amplify/data/resource.ts +10 -0
- package/amplify/data/schema.js +10 -0
- package/amplify/data/schema.ts +10 -0
- package/amplify/functions/category-action/handler.ts +2 -1
- package/amplify/functions/content-actions/requirements.txt +1 -1
- package/amplify/functions/content-actions/resource.js +1 -1
- package/amplify/functions/content-actions/resource.ts +1 -1
- package/amplify/functions/renditions/requirements.txt +1 -1
- package/amplify/functions/shared/reference-curation-metadata.ts +15 -0
- package/amplify/publication/renditions-function.js +1 -1
- package/amplify/publication/renditions-function.ts +1 -1
- package/components/board-card-excerpts.tsx +1 -1
- package/components/newsroom-newsprint-board.css +1 -1
- package/components/newsroom-organizer-shell.tsx +4 -1
- package/components/newsroom-organizer.css +9 -0
- package/components/prospectus-account-chooser.tsx +98 -0
- package/lib/content-source-context.ts +9 -1
- package/lib/graphql-content-repository.ts +42 -7
- package/lib/local-preview-seed.ts +22 -2
- package/lib/local-preview-tenant.ts +27 -0
- package/lib/prospectus-tenant.ts +22 -0
- package/lib/tenant-keys.ts +32 -0
- package/package.json +5 -1
- package/routes.manifest.json +1 -1
package/amplify/data/resource.js
CHANGED
|
@@ -1018,6 +1018,7 @@ const schema = a.schema({
|
|
|
1018
1018
|
NewsroomSection: a
|
|
1019
1019
|
.model({
|
|
1020
1020
|
id: a.id().required(),
|
|
1021
|
+
tenantId: a.string(),
|
|
1021
1022
|
title: a.string().required(),
|
|
1022
1023
|
type: a.string().required(),
|
|
1023
1024
|
editorialMission: a.string().required(),
|
|
@@ -1036,6 +1037,7 @@ const schema = a.schema({
|
|
|
1036
1037
|
})
|
|
1037
1038
|
.secondaryIndexes((index) => [
|
|
1038
1039
|
index("sortOrder").sortKeys(["id"]).queryField("listNewsroomSectionsBySortOrder"),
|
|
1040
|
+
index("tenantId").sortKeys(["sortOrder"]).queryField("listNewsroomSectionsByTenantAndSortOrder"),
|
|
1039
1041
|
index("type").sortKeys(["sortOrder"]).queryField("listNewsroomSectionsByTypeAndSortOrder"),
|
|
1040
1042
|
index("enabledStatus").sortKeys(["sortOrder"]).queryField("listNewsroomSectionsByEnabledStatusAndSortOrder"),
|
|
1041
1043
|
])
|
|
@@ -1624,6 +1626,7 @@ const schema = a.schema({
|
|
|
1624
1626
|
.model({
|
|
1625
1627
|
id: a.id().required(),
|
|
1626
1628
|
lineageId: a.id().required(),
|
|
1629
|
+
tenantId: a.string(),
|
|
1627
1630
|
versionNumber: a.integer().required(),
|
|
1628
1631
|
previousVersionId: a.id(),
|
|
1629
1632
|
versionState: a.string().required(),
|
|
@@ -1636,6 +1639,7 @@ const schema = a.schema({
|
|
|
1636
1639
|
typeStatus: a.string().required(),
|
|
1637
1640
|
stage: a.string(),
|
|
1638
1641
|
slug: a.string().required(),
|
|
1642
|
+
tenantSlug: a.string(),
|
|
1639
1643
|
shortSlug: a.string(),
|
|
1640
1644
|
section: a.string(),
|
|
1641
1645
|
sectionStatus: a.string(),
|
|
@@ -1663,6 +1667,7 @@ const schema = a.schema({
|
|
|
1663
1667
|
.secondaryIndexes((index) => [
|
|
1664
1668
|
index("lineageId").sortKeys(["versionNumber"]).queryField("listItemsByLineageAndVersion"),
|
|
1665
1669
|
index("slug").queryField("itemBySlug"),
|
|
1670
|
+
index("tenantSlug").queryField("itemByTenantSlug"),
|
|
1666
1671
|
index("typeStatus").sortKeys(["publishedAt"]).queryField("listItemsByTypeStatusAndPublishedAt"),
|
|
1667
1672
|
index("sectionStatus").sortKeys(["publishedAt"]).queryField("listItemsBySectionStatusAndPublishedAt"),
|
|
1668
1673
|
index("versionState").sortKeys(["updatedAt"]).queryField("listItemsByVersionStateAndUpdatedAt"),
|
|
@@ -1710,6 +1715,7 @@ const schema = a.schema({
|
|
|
1710
1715
|
.model({
|
|
1711
1716
|
id: a.id().required(),
|
|
1712
1717
|
itemId: a.id().required(),
|
|
1718
|
+
tenantId: a.string(),
|
|
1713
1719
|
type: a.string().required(),
|
|
1714
1720
|
role: a.string().required(),
|
|
1715
1721
|
sortKey: a.string().required(),
|
|
@@ -1822,11 +1828,13 @@ const schema = a.schema({
|
|
|
1822
1828
|
id: a.id().required(),
|
|
1823
1829
|
sourceItemId: a.id().required(),
|
|
1824
1830
|
itemLineageId: a.id().required(),
|
|
1831
|
+
tenantId: a.string(),
|
|
1825
1832
|
versionNumber: a.integer().required(),
|
|
1826
1833
|
type: a.string().required(),
|
|
1827
1834
|
status: a.string().required(),
|
|
1828
1835
|
typeStatus: a.string().required(),
|
|
1829
1836
|
slug: a.string().required(),
|
|
1837
|
+
tenantSlug: a.string(),
|
|
1830
1838
|
shortSlug: a.string(),
|
|
1831
1839
|
section: a.string(),
|
|
1832
1840
|
sectionStatus: a.string(),
|
|
@@ -1849,6 +1857,7 @@ const schema = a.schema({
|
|
|
1849
1857
|
})
|
|
1850
1858
|
.secondaryIndexes((index) => [
|
|
1851
1859
|
index("slug").queryField("publishedItemBySlug"),
|
|
1860
|
+
index("tenantSlug").queryField("publishedItemByTenantSlug"),
|
|
1852
1861
|
index("typeStatus").sortKeys(["publishedAt"]).queryField("listPublishedItemsByTypeStatusAndPublishedAt"),
|
|
1853
1862
|
index("sectionStatus").sortKeys(["publishedAt"]).queryField("listPublishedItemsBySectionStatusAndPublishedAt"),
|
|
1854
1863
|
index("itemLineageId").sortKeys(["versionNumber"]).queryField("listPublishedItemsByLineageAndVersion"),
|
|
@@ -1861,6 +1870,7 @@ const schema = a.schema({
|
|
|
1861
1870
|
.model({
|
|
1862
1871
|
id: a.id().required(),
|
|
1863
1872
|
sourceMediaAssetId: a.id().required(),
|
|
1873
|
+
tenantId: a.string(),
|
|
1864
1874
|
publishedItemId: a.id().required(),
|
|
1865
1875
|
sourceItemId: a.id().required(),
|
|
1866
1876
|
itemLineageId: a.id().required(),
|
package/amplify/data/resource.ts
CHANGED
|
@@ -1100,6 +1100,7 @@ const schema = a.schema({
|
|
|
1100
1100
|
NewsroomSection: a
|
|
1101
1101
|
.model({
|
|
1102
1102
|
id: a.id().required(),
|
|
1103
|
+
tenantId: a.string(),
|
|
1103
1104
|
title: a.string().required(),
|
|
1104
1105
|
type: a.string().required(),
|
|
1105
1106
|
editorialMission: a.string().required(),
|
|
@@ -1118,6 +1119,7 @@ const schema = a.schema({
|
|
|
1118
1119
|
})
|
|
1119
1120
|
.secondaryIndexes((index) => [
|
|
1120
1121
|
index("sortOrder").sortKeys(["id"]).queryField("listNewsroomSectionsBySortOrder"),
|
|
1122
|
+
index("tenantId").sortKeys(["sortOrder"]).queryField("listNewsroomSectionsByTenantAndSortOrder"),
|
|
1121
1123
|
index("type").sortKeys(["sortOrder"]).queryField("listNewsroomSectionsByTypeAndSortOrder"),
|
|
1122
1124
|
index("enabledStatus").sortKeys(["sortOrder"]).queryField("listNewsroomSectionsByEnabledStatusAndSortOrder"),
|
|
1123
1125
|
])
|
|
@@ -1725,6 +1727,7 @@ const schema = a.schema({
|
|
|
1725
1727
|
.model({
|
|
1726
1728
|
id: a.id().required(),
|
|
1727
1729
|
lineageId: a.id().required(),
|
|
1730
|
+
tenantId: a.string(),
|
|
1728
1731
|
versionNumber: a.integer().required(),
|
|
1729
1732
|
previousVersionId: a.id(),
|
|
1730
1733
|
versionState: a.string().required(),
|
|
@@ -1737,6 +1740,7 @@ const schema = a.schema({
|
|
|
1737
1740
|
typeStatus: a.string().required(),
|
|
1738
1741
|
stage: a.string(),
|
|
1739
1742
|
slug: a.string().required(),
|
|
1743
|
+
tenantSlug: a.string(),
|
|
1740
1744
|
shortSlug: a.string(),
|
|
1741
1745
|
section: a.string(),
|
|
1742
1746
|
sectionStatus: a.string(),
|
|
@@ -1764,6 +1768,7 @@ const schema = a.schema({
|
|
|
1764
1768
|
.secondaryIndexes((index) => [
|
|
1765
1769
|
index("lineageId").sortKeys(["versionNumber"]).queryField("listItemsByLineageAndVersion"),
|
|
1766
1770
|
index("slug").queryField("itemBySlug"),
|
|
1771
|
+
index("tenantSlug").queryField("itemByTenantSlug"),
|
|
1767
1772
|
index("typeStatus").sortKeys(["publishedAt"]).queryField("listItemsByTypeStatusAndPublishedAt"),
|
|
1768
1773
|
index("sectionStatus").sortKeys(["publishedAt"]).queryField("listItemsBySectionStatusAndPublishedAt"),
|
|
1769
1774
|
index("versionState").sortKeys(["updatedAt"]).queryField("listItemsByVersionStateAndUpdatedAt"),
|
|
@@ -1814,6 +1819,7 @@ const schema = a.schema({
|
|
|
1814
1819
|
.model({
|
|
1815
1820
|
id: a.id().required(),
|
|
1816
1821
|
itemId: a.id().required(),
|
|
1822
|
+
tenantId: a.string(),
|
|
1817
1823
|
type: a.string().required(),
|
|
1818
1824
|
role: a.string().required(),
|
|
1819
1825
|
sortKey: a.string().required(),
|
|
@@ -1930,11 +1936,13 @@ const schema = a.schema({
|
|
|
1930
1936
|
id: a.id().required(),
|
|
1931
1937
|
sourceItemId: a.id().required(),
|
|
1932
1938
|
itemLineageId: a.id().required(),
|
|
1939
|
+
tenantId: a.string(),
|
|
1933
1940
|
versionNumber: a.integer().required(),
|
|
1934
1941
|
type: a.string().required(),
|
|
1935
1942
|
status: a.string().required(),
|
|
1936
1943
|
typeStatus: a.string().required(),
|
|
1937
1944
|
slug: a.string().required(),
|
|
1945
|
+
tenantSlug: a.string(),
|
|
1938
1946
|
shortSlug: a.string(),
|
|
1939
1947
|
section: a.string(),
|
|
1940
1948
|
sectionStatus: a.string(),
|
|
@@ -1957,6 +1965,7 @@ const schema = a.schema({
|
|
|
1957
1965
|
})
|
|
1958
1966
|
.secondaryIndexes((index) => [
|
|
1959
1967
|
index("slug").queryField("publishedItemBySlug"),
|
|
1968
|
+
index("tenantSlug").queryField("publishedItemByTenantSlug"),
|
|
1960
1969
|
index("typeStatus").sortKeys(["publishedAt"]).queryField("listPublishedItemsByTypeStatusAndPublishedAt"),
|
|
1961
1970
|
index("sectionStatus").sortKeys(["publishedAt"]).queryField("listPublishedItemsBySectionStatusAndPublishedAt"),
|
|
1962
1971
|
index("itemLineageId").sortKeys(["versionNumber"]).queryField("listPublishedItemsByLineageAndVersion"),
|
|
@@ -1970,6 +1979,7 @@ const schema = a.schema({
|
|
|
1970
1979
|
.model({
|
|
1971
1980
|
id: a.id().required(),
|
|
1972
1981
|
sourceMediaAssetId: a.id().required(),
|
|
1982
|
+
tenantId: a.string(),
|
|
1973
1983
|
publishedItemId: a.id().required(),
|
|
1974
1984
|
sourceItemId: a.id().required(),
|
|
1975
1985
|
itemLineageId: a.id().required(),
|
package/amplify/data/schema.js
CHANGED
|
@@ -918,6 +918,7 @@ const baseDefinitions = {
|
|
|
918
918
|
]),
|
|
919
919
|
NewsroomSection: modelWithoutSubscriptions({
|
|
920
920
|
id: a.id().required(),
|
|
921
|
+
tenantId: a.string(),
|
|
921
922
|
title: a.string().required(),
|
|
922
923
|
type: a.string().required(),
|
|
923
924
|
editorialMission: a.string().required(),
|
|
@@ -936,6 +937,7 @@ const baseDefinitions = {
|
|
|
936
937
|
})
|
|
937
938
|
.secondaryIndexes((index) => [
|
|
938
939
|
index("sortOrder").sortKeys(["id"]).queryField("listNewsroomSectionsBySortOrder"),
|
|
940
|
+
index("tenantId").sortKeys(["sortOrder"]).queryField("listNewsroomSectionsByTenantAndSortOrder"),
|
|
939
941
|
index("type").sortKeys(["sortOrder"]).queryField("listNewsroomSectionsByTypeAndSortOrder"),
|
|
940
942
|
index("enabledStatus").sortKeys(["sortOrder"]).queryField("listNewsroomSectionsByEnabledStatusAndSortOrder"),
|
|
941
943
|
])
|
|
@@ -1503,6 +1505,7 @@ const baseDefinitions = {
|
|
|
1503
1505
|
Item: modelWithoutSubscriptions({
|
|
1504
1506
|
id: a.id().required(),
|
|
1505
1507
|
lineageId: a.id().required(),
|
|
1508
|
+
tenantId: a.string(),
|
|
1506
1509
|
versionNumber: a.integer().required(),
|
|
1507
1510
|
previousVersionId: a.id(),
|
|
1508
1511
|
versionState: a.string().required(),
|
|
@@ -1515,6 +1518,7 @@ const baseDefinitions = {
|
|
|
1515
1518
|
status: a.string().required(),
|
|
1516
1519
|
typeStatus: a.string().required(),
|
|
1517
1520
|
slug: a.string().required(),
|
|
1521
|
+
tenantSlug: a.string(),
|
|
1518
1522
|
shortSlug: a.string(),
|
|
1519
1523
|
section: a.string(),
|
|
1520
1524
|
sectionStatus: a.string(),
|
|
@@ -1542,6 +1546,7 @@ const baseDefinitions = {
|
|
|
1542
1546
|
.secondaryIndexes((index) => [
|
|
1543
1547
|
index("lineageId").sortKeys(["versionNumber"]).queryField("listItemsByLineageAndVersion"),
|
|
1544
1548
|
index("slug").queryField("itemBySlug"),
|
|
1549
|
+
index("tenantSlug").queryField("itemByTenantSlug"),
|
|
1545
1550
|
index("typeStatus").sortKeys(["publishedAt"]).queryField("listItemsByTypeStatusAndPublishedAt"),
|
|
1546
1551
|
index("sectionStatus").sortKeys(["publishedAt"]).queryField("listItemsBySectionStatusAndPublishedAt"),
|
|
1547
1552
|
index("versionState").sortKeys(["updatedAt"]).queryField("listItemsByVersionStateAndUpdatedAt"),
|
|
@@ -1586,6 +1591,7 @@ const baseDefinitions = {
|
|
|
1586
1591
|
MediaAsset: modelWithoutSubscriptions({
|
|
1587
1592
|
id: a.id().required(),
|
|
1588
1593
|
itemId: a.id().required(),
|
|
1594
|
+
tenantId: a.string(),
|
|
1589
1595
|
type: a.string().required(),
|
|
1590
1596
|
role: a.string().required(),
|
|
1591
1597
|
sortKey: a.string().required(),
|
|
@@ -1669,11 +1675,13 @@ const baseDefinitions = {
|
|
|
1669
1675
|
id: a.id().required(),
|
|
1670
1676
|
sourceItemId: a.id().required(),
|
|
1671
1677
|
itemLineageId: a.id().required(),
|
|
1678
|
+
tenantId: a.string(),
|
|
1672
1679
|
versionNumber: a.integer().required(),
|
|
1673
1680
|
type: a.string().required(),
|
|
1674
1681
|
status: a.string().required(),
|
|
1675
1682
|
typeStatus: a.string().required(),
|
|
1676
1683
|
slug: a.string().required(),
|
|
1684
|
+
tenantSlug: a.string(),
|
|
1677
1685
|
shortSlug: a.string(),
|
|
1678
1686
|
section: a.string(),
|
|
1679
1687
|
sectionStatus: a.string(),
|
|
@@ -1696,6 +1704,7 @@ const baseDefinitions = {
|
|
|
1696
1704
|
})
|
|
1697
1705
|
.secondaryIndexes((index) => [
|
|
1698
1706
|
index("slug").queryField("publishedItemBySlug"),
|
|
1707
|
+
index("tenantSlug").queryField("publishedItemByTenantSlug"),
|
|
1699
1708
|
index("typeStatus").sortKeys(["publishedAt"]).queryField("listPublishedItemsByTypeStatusAndPublishedAt"),
|
|
1700
1709
|
index("sectionStatus").sortKeys(["publishedAt"]).queryField("listPublishedItemsBySectionStatusAndPublishedAt"),
|
|
1701
1710
|
index("itemLineageId").sortKeys(["versionNumber"]).queryField("listPublishedItemsByLineageAndVersion"),
|
|
@@ -1707,6 +1716,7 @@ const baseDefinitions = {
|
|
|
1707
1716
|
PublishedMediaAsset: modelWithoutSubscriptions({
|
|
1708
1717
|
id: a.id().required(),
|
|
1709
1718
|
sourceMediaAssetId: a.id().required(),
|
|
1719
|
+
tenantId: a.string(),
|
|
1710
1720
|
publishedItemId: a.id().required(),
|
|
1711
1721
|
sourceItemId: a.id().required(),
|
|
1712
1722
|
itemLineageId: a.id().required(),
|
package/amplify/data/schema.ts
CHANGED
|
@@ -994,6 +994,7 @@ const baseDefinitions = {
|
|
|
994
994
|
|
|
995
995
|
NewsroomSection: modelWithoutSubscriptions({
|
|
996
996
|
id: a.id().required(),
|
|
997
|
+
tenantId: a.string(),
|
|
997
998
|
title: a.string().required(),
|
|
998
999
|
type: a.string().required(),
|
|
999
1000
|
editorialMission: a.string().required(),
|
|
@@ -1012,6 +1013,7 @@ const baseDefinitions = {
|
|
|
1012
1013
|
})
|
|
1013
1014
|
.secondaryIndexes((index) => [
|
|
1014
1015
|
index("sortOrder").sortKeys(["id"]).queryField("listNewsroomSectionsBySortOrder"),
|
|
1016
|
+
index("tenantId").sortKeys(["sortOrder"]).queryField("listNewsroomSectionsByTenantAndSortOrder"),
|
|
1015
1017
|
index("type").sortKeys(["sortOrder"]).queryField("listNewsroomSectionsByTypeAndSortOrder"),
|
|
1016
1018
|
index("enabledStatus").sortKeys(["sortOrder"]).queryField("listNewsroomSectionsByEnabledStatusAndSortOrder"),
|
|
1017
1019
|
])
|
|
@@ -1598,6 +1600,7 @@ const baseDefinitions = {
|
|
|
1598
1600
|
Item: modelWithoutSubscriptions({
|
|
1599
1601
|
id: a.id().required(),
|
|
1600
1602
|
lineageId: a.id().required(),
|
|
1603
|
+
tenantId: a.string(),
|
|
1601
1604
|
versionNumber: a.integer().required(),
|
|
1602
1605
|
previousVersionId: a.id(),
|
|
1603
1606
|
versionState: a.string().required(),
|
|
@@ -1610,6 +1613,7 @@ const baseDefinitions = {
|
|
|
1610
1613
|
status: a.string().required(),
|
|
1611
1614
|
typeStatus: a.string().required(),
|
|
1612
1615
|
slug: a.string().required(),
|
|
1616
|
+
tenantSlug: a.string(),
|
|
1613
1617
|
shortSlug: a.string(),
|
|
1614
1618
|
section: a.string(),
|
|
1615
1619
|
sectionStatus: a.string(),
|
|
@@ -1637,6 +1641,7 @@ const baseDefinitions = {
|
|
|
1637
1641
|
.secondaryIndexes((index) => [
|
|
1638
1642
|
index("lineageId").sortKeys(["versionNumber"]).queryField("listItemsByLineageAndVersion"),
|
|
1639
1643
|
index("slug").queryField("itemBySlug"),
|
|
1644
|
+
index("tenantSlug").queryField("itemByTenantSlug"),
|
|
1640
1645
|
index("typeStatus").sortKeys(["publishedAt"]).queryField("listItemsByTypeStatusAndPublishedAt"),
|
|
1641
1646
|
index("sectionStatus").sortKeys(["publishedAt"]).queryField("listItemsBySectionStatusAndPublishedAt"),
|
|
1642
1647
|
index("versionState").sortKeys(["updatedAt"]).queryField("listItemsByVersionStateAndUpdatedAt"),
|
|
@@ -1684,6 +1689,7 @@ const baseDefinitions = {
|
|
|
1684
1689
|
MediaAsset: modelWithoutSubscriptions({
|
|
1685
1690
|
id: a.id().required(),
|
|
1686
1691
|
itemId: a.id().required(),
|
|
1692
|
+
tenantId: a.string(),
|
|
1687
1693
|
type: a.string().required(),
|
|
1688
1694
|
role: a.string().required(),
|
|
1689
1695
|
sortKey: a.string().required(),
|
|
@@ -1770,11 +1776,13 @@ const baseDefinitions = {
|
|
|
1770
1776
|
id: a.id().required(),
|
|
1771
1777
|
sourceItemId: a.id().required(),
|
|
1772
1778
|
itemLineageId: a.id().required(),
|
|
1779
|
+
tenantId: a.string(),
|
|
1773
1780
|
versionNumber: a.integer().required(),
|
|
1774
1781
|
type: a.string().required(),
|
|
1775
1782
|
status: a.string().required(),
|
|
1776
1783
|
typeStatus: a.string().required(),
|
|
1777
1784
|
slug: a.string().required(),
|
|
1785
|
+
tenantSlug: a.string(),
|
|
1778
1786
|
shortSlug: a.string(),
|
|
1779
1787
|
section: a.string(),
|
|
1780
1788
|
sectionStatus: a.string(),
|
|
@@ -1797,6 +1805,7 @@ const baseDefinitions = {
|
|
|
1797
1805
|
})
|
|
1798
1806
|
.secondaryIndexes((index) => [
|
|
1799
1807
|
index("slug").queryField("publishedItemBySlug"),
|
|
1808
|
+
index("tenantSlug").queryField("publishedItemByTenantSlug"),
|
|
1800
1809
|
index("typeStatus").sortKeys(["publishedAt"]).queryField("listPublishedItemsByTypeStatusAndPublishedAt"),
|
|
1801
1810
|
index("sectionStatus").sortKeys(["publishedAt"]).queryField("listPublishedItemsBySectionStatusAndPublishedAt"),
|
|
1802
1811
|
index("itemLineageId").sortKeys(["versionNumber"]).queryField("listPublishedItemsByLineageAndVersion"),
|
|
@@ -1809,6 +1818,7 @@ const baseDefinitions = {
|
|
|
1809
1818
|
PublishedMediaAsset: modelWithoutSubscriptions({
|
|
1810
1819
|
id: a.id().required(),
|
|
1811
1820
|
sourceMediaAssetId: a.id().required(),
|
|
1821
|
+
tenantId: a.string(),
|
|
1812
1822
|
publishedItemId: a.id().required(),
|
|
1813
1823
|
sourceItemId: a.id().required(),
|
|
1814
1824
|
itemLineageId: a.id().required(),
|
|
@@ -4,7 +4,7 @@ import { Amplify } from "aws-amplify";
|
|
|
4
4
|
import { generateClient } from "aws-amplify/data";
|
|
5
5
|
import type { Schema } from "../../data/resource";
|
|
6
6
|
import { putJsonModelPayload, putTextModelPayload, readJsonModelPayload } from "../shared/model-payloads";
|
|
7
|
-
import { referenceCurationMetadata } from "../shared/reference-curation-metadata";
|
|
7
|
+
import { curationActorKind, referenceCurationMetadata } from "../shared/reference-curation-metadata";
|
|
8
8
|
|
|
9
9
|
type ReviewHandler = Schema["reviewSteeringProposal"]["functionHandler"];
|
|
10
10
|
type ReferenceCurationHandler = Schema["reviewReferenceCuration"]["functionHandler"];
|
|
@@ -234,6 +234,7 @@ async function reviewReferenceCuration(event: Parameters<ReferenceCurationHandle
|
|
|
234
234
|
referenceLineageId,
|
|
235
235
|
decisionRelationId: normalizeOptionalString(event.arguments.decisionRelationId),
|
|
236
236
|
shareable: event.arguments.shareable ?? null,
|
|
237
|
+
actorKind: curationActorKind(event.identity),
|
|
237
238
|
}),
|
|
238
239
|
{ filename: "metadata.json", now },
|
|
239
240
|
);
|
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { pythonLambdaCode } from "../shared/python-bundle.js";
|
|
8
8
|
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const projectRoot = path.resolve(dirname, "../../..");
|
|
10
|
-
export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.
|
|
10
|
+
export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.7.0", "Pillow>=11.3,<12", "awscrt>=0.23,<1"];
|
|
11
11
|
function installCheckoutRequirements(outputDir) {
|
|
12
12
|
const requirementsPath = path.join(projectRoot, "amplify/functions/content-actions/requirements.txt");
|
|
13
13
|
execSync(`python3 -m pip install -r "${requirementsPath}" -t "${outputDir}" --no-cache-dir --platform manylinux2014_aarch64 --implementation cp --python-version 3.12 --only-binary=:all: --ignore-requires-python`, { cwd: projectRoot, stdio: "inherit" });
|
|
@@ -10,7 +10,7 @@ import { pythonLambdaCode, type SiteBundleFile } from "../shared/python-bundle";
|
|
|
10
10
|
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
const projectRoot = path.resolve(dirname, "../../..");
|
|
12
12
|
|
|
13
|
-
export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.
|
|
13
|
+
export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.7.0", "Pillow>=11.3,<12", "awscrt>=0.23,<1"];
|
|
14
14
|
|
|
15
15
|
function installCheckoutRequirements(outputDir: string): void {
|
|
16
16
|
const requirementsPath = path.join(projectRoot, "amplify/functions/content-actions/requirements.txt");
|
|
@@ -12,6 +12,20 @@ export type ReferenceCurationMetadataInput = {
|
|
|
12
12
|
referenceLineageId: string
|
|
13
13
|
decisionRelationId?: string | null
|
|
14
14
|
shareable?: boolean | null
|
|
15
|
+
actorKind?: CurationActorKind | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type CurationActorKind = "human" | "service"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A curation is human only when the caller signed in through the user pool, so
|
|
22
|
+
* AppSync supplied a verified subject claim. Request arguments never decide it;
|
|
23
|
+
* IAM-signed service callers and agents have no such claim.
|
|
24
|
+
*/
|
|
25
|
+
export function curationActorKind(identity: unknown): CurationActorKind {
|
|
26
|
+
const claims = (identity as {claims?: Record<string, unknown>} | null | undefined)?.claims
|
|
27
|
+
const subject = claims?.sub
|
|
28
|
+
return typeof subject === "string" && subject.trim() !== "" ? "human" : "service"
|
|
15
29
|
}
|
|
16
30
|
|
|
17
31
|
export function referenceCurationMetadata(input: ReferenceCurationMetadataInput): Record<string, unknown> {
|
|
@@ -23,6 +37,7 @@ export function referenceCurationMetadata(input: ReferenceCurationMetadataInput)
|
|
|
23
37
|
curationReasonCode: input.reasonCode,
|
|
24
38
|
referenceId: input.referenceId,
|
|
25
39
|
referenceLineageId: input.referenceLineageId,
|
|
40
|
+
...(input.actorKind ? {actorKind: input.actorKind} : {}),
|
|
26
41
|
...(decisionRelationId ? {decisionRelationId} : {}),
|
|
27
42
|
...(decisionRelationId ? {shareable: input.shareable === true} : {}),
|
|
28
43
|
}
|
|
@@ -11,7 +11,7 @@ import { pythonLambdaCode } from "../functions/shared/python-bundle.js";
|
|
|
11
11
|
import { RENDITIONS_EPHEMERAL_STORAGE_MB, RENDITIONS_MEDIA_PREFIX, RENDITIONS_MEMORY_MB, RENDITIONS_TIMEOUT_SECONDS, renditionsEnvironment, renditionsEventPattern, } from "./renditions-settings.js";
|
|
12
12
|
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
const projectRoot = path.resolve(dirname, "../..");
|
|
14
|
-
export const renditionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.
|
|
14
|
+
export const renditionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.7.0", "Pillow>=11.3,<12"];
|
|
15
15
|
function installCheckoutRequirements(outputDir) {
|
|
16
16
|
const requirementsPath = path.join(projectRoot, "amplify/functions/renditions/requirements.txt");
|
|
17
17
|
execSync(`python3 -m pip install -r "${requirementsPath}" -t "${outputDir}" --no-cache-dir --platform manylinux2014_aarch64 --implementation cp --python-version 3.12 --only-binary=:all: --ignore-requires-python`, { cwd: projectRoot, stdio: "inherit" });
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
23
|
const projectRoot = path.resolve(dirname, "../..");
|
|
24
24
|
|
|
25
|
-
export const renditionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.
|
|
25
|
+
export const renditionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.7.0", "Pillow>=11.3,<12"];
|
|
26
26
|
|
|
27
27
|
export type RenditionsFunctionProps = {
|
|
28
28
|
siteBucket: s3.IBucket;
|
|
@@ -9,7 +9,7 @@ function CardExcerpt({ text }: { text: string }) {
|
|
|
9
9
|
const [expanded, setExpanded] = useState(false);
|
|
10
10
|
return <div className="board-card-excerpt" data-board-card-excerpt data-expanded={expanded}>
|
|
11
11
|
<p>{text}</p>
|
|
12
|
-
{text.length >
|
|
12
|
+
{text.length > 130 && <button aria-expanded={expanded} onClick={event => { event.stopPropagation(); setExpanded(open => !open); }} onKeyDown={event => event.stopPropagation()} type="button">{expanded ? "Less" : "More"}</button>}
|
|
13
13
|
</div>;
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -51,6 +51,6 @@
|
|
|
51
51
|
.papyrus-newsprint-board .issue-card .issue-accent-bar { height: auto !important; margin: 0 !important; width: 100% !important; padding: 0 !important; min-height: 0; }
|
|
52
52
|
.papyrus-newsprint-board .issue-card .issue-card-content { padding-top: 2px !important; gap: 4px; }
|
|
53
53
|
.papyrus-newsprint-board .issue-card h3 { margin: 0; }
|
|
54
|
-
.board-card-excerpt p { margin: 0; font-size: 13px; line-height: 1.4; color: var(--org-ink); display: -webkit-box; -webkit-line-clamp:
|
|
54
|
+
.board-card-excerpt p { margin: 0; font-size: 13px; line-height: 1.4; color: var(--org-ink); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
|
55
55
|
.board-card-excerpt[data-expanded="true"] p { display: block; -webkit-line-clamp: unset; }
|
|
56
56
|
.board-card-excerpt button { background: none; border: 0; padding: 0; font: inherit; font-size: 12px; text-decoration: underline; text-underline-offset: 3px; color: var(--org-ink); cursor: pointer; }
|
|
@@ -10,6 +10,7 @@ import { getNewsroomNavHref } from "../lib/newsroom-nav";
|
|
|
10
10
|
import { SITE_BRAND } from "../lib/site-brand";
|
|
11
11
|
import { useOptionalNewsDeskClient } from "./news-desk-client-provider";
|
|
12
12
|
import { ReaderAuthControl } from "./reader-auth-control";
|
|
13
|
+
import { ProspectusAccountChooser, useProspectusAccountSummary } from "./prospectus-account-chooser";
|
|
13
14
|
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetTrigger } from "./ui/sheet";
|
|
14
15
|
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuItem, DropdownMenuSeparator } from "./ui/dropdown-menu";
|
|
15
16
|
import type { NewsroomOpsShellProps } from "./newsroom-ops-shell";
|
|
@@ -54,6 +55,7 @@ function Account({ placement }: { placement: "sidebar" | "header" }) {
|
|
|
54
55
|
const session = useOptionalNewsDeskClient();
|
|
55
56
|
const auth = session?.shell.auth ?? { status: "loading" as const, label: "Checking sign-in" };
|
|
56
57
|
const [open, setOpen] = useState(false);
|
|
58
|
+
const accountSummary = useProspectusAccountSummary();
|
|
57
59
|
useEffect(() => {
|
|
58
60
|
const media = matchMedia("(max-width: 900px)");
|
|
59
61
|
const close = () => setOpen(false);
|
|
@@ -62,11 +64,12 @@ function Account({ placement }: { placement: "sidebar" | "header" }) {
|
|
|
62
64
|
}, []);
|
|
63
65
|
return <DropdownMenu open={open} onOpenChange={setOpen}>
|
|
64
66
|
<DropdownMenuTrigger id={`organizer-account-trigger-${placement}`} className={`organizer-account organizer-account-${placement}`} aria-label="Account menu" title="Account menu">
|
|
65
|
-
<UserRound size={18} aria-hidden="true" /><span><strong>{auth.label}</strong><small>{auth.status === "signedIn" ? "Account & preferences" : auth.status === "signedOut" ? "Not signed in" : "Checking session"}</small></span>
|
|
67
|
+
<UserRound size={18} aria-hidden="true" /><span><strong>{auth.label}</strong><small>{accountSummary ?? (auth.status === "signedIn" ? "Account & preferences" : auth.status === "signedOut" ? "Not signed in" : "Checking session")}</small></span>
|
|
66
68
|
</DropdownMenuTrigger>
|
|
67
69
|
<DropdownMenuContent className="organizer-account-popup" side={placement === "sidebar" ? "top" : "bottom"} align="end">
|
|
68
70
|
<DropdownMenuGroup>
|
|
69
71
|
<DropdownMenuLabel>{auth.label}</DropdownMenuLabel><DropdownMenuSeparator />
|
|
72
|
+
<ProspectusAccountChooser />
|
|
70
73
|
<DropdownMenuItem render={<Link href={getNewsroomNavHref(ORGANIZER_SETTINGS_DESTINATION.path)} />}><Settings aria-hidden="true" />{ORGANIZER_SETTINGS_DESTINATION.label}</DropdownMenuItem>
|
|
71
74
|
<ReaderAuthControl authState={auth} renderAction={({ disabled, onClick, signedIn }) => <DropdownMenuItem disabled={disabled} closeOnClick={false} onClick={onClick}>
|
|
72
75
|
{signedIn ? <LogOut aria-hidden="true" /> : <LogIn aria-hidden="true" />}{signedIn ? "Sign out" : "Sign in"}
|
|
@@ -265,3 +265,12 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
|
|
|
265
265
|
.news-desk-columns--taxonomy .category-steering-category-card button{display:inline-flex;align-items:center;justify-content:center;min-height:36px;padding:4px 12px;border:2px solid var(--org-ink);border-radius:0;background:transparent;color:var(--org-ink);font:700 12px/1.2 Newsroom UI,system-ui,sans-serif;letter-spacing:.05em;text-transform:uppercase;cursor:pointer}
|
|
266
266
|
.news-desk-columns--taxonomy .category-steering-category-card button:disabled{border-color:var(--org-line);opacity:.6;cursor:default}
|
|
267
267
|
.news-desk-columns--taxonomy .news-desk-rail-column .category-steering-section__header{border-bottom:0;padding-bottom:4px}
|
|
268
|
+
.organizer-account-chooser{padding:8px 12px;border-bottom:1px solid var(--org-line);display:grid;gap:6px}
|
|
269
|
+
.organizer-account-chooser ul{list-style:none;margin:0;padding:0;display:grid}
|
|
270
|
+
.organizer-account-chooser li button{display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%;text-align:left;background:none;border:0;border-left:3px solid transparent;padding:6px 8px;font:inherit;color:var(--org-ink);cursor:pointer}
|
|
271
|
+
.organizer-account-chooser li button[data-selected]{border-left-color:var(--org-ink);font-weight:700}
|
|
272
|
+
.organizer-account-chooser li button:disabled{opacity:.6;cursor:default}
|
|
273
|
+
.organizer-account-chooser li button span{display:grid;gap:1px}
|
|
274
|
+
.organizer-account-chooser li button small,.organizer-account-chooser-note{font-size:12px;color:var(--org-ink);margin:0}
|
|
275
|
+
.organizer-account-chooser-heading{margin:0;font:600 11px/1.4 Newsroom UI,system-ui,sans-serif;letter-spacing:.07em;text-transform:uppercase}
|
|
276
|
+
.organizer-account-chooser-link{color:var(--org-ink);text-decoration:underline;text-underline-offset:3px}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { Check } from "lucide-react";
|
|
5
|
+
import { isLocalPreviewEnabled } from "../lib/local-preview";
|
|
6
|
+
import { LOCAL_PREVIEW_TENANTS, getLocalPreviewTenantId, setLocalPreviewTenantId } from "../lib/local-preview-tenant";
|
|
7
|
+
import { loadProspectusAccountState, selectProspectusAccount, type ProspectusAccountState } from "../lib/prospectus-account-client";
|
|
8
|
+
|
|
9
|
+
function previewAccountState(): ProspectusAccountState {
|
|
10
|
+
return {
|
|
11
|
+
status: "signedIn",
|
|
12
|
+
publishingAccounts: LOCAL_PREVIEW_TENANTS.map(tenant => ({ accountId: tenant.accountId, name: tenant.name, role: tenant.role })),
|
|
13
|
+
selectedAccountId: getLocalPreviewTenantId(),
|
|
14
|
+
cyclotronLabAccess: true,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const ACCOUNT_STATE_EVENT = "papyrus:prospectus-account-state";
|
|
19
|
+
|
|
20
|
+
function summaryOf(state: ProspectusAccountState): string | null {
|
|
21
|
+
if (state.status !== "signedIn") return null;
|
|
22
|
+
const selected = state.publishingAccounts.find(account => account.accountId === state.selectedAccountId);
|
|
23
|
+
return selected ? `${selected.name ?? selected.accountId} · ${roleLabel(selected.role)}` : "Choose an account";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const roleLabel = (role: string) => role.replace(/_/g, " ");
|
|
27
|
+
|
|
28
|
+
/** The signed-in person's current publishing account as "Name · role", or null when the site has no Prospectus accounts. */
|
|
29
|
+
export function useProspectusAccountSummary(): string | null {
|
|
30
|
+
const [summary, setSummary] = useState<string | null>(null);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
let cancelled = false;
|
|
33
|
+
const returnPath = `${window.location.pathname}${window.location.search}`;
|
|
34
|
+
(isLocalPreviewEnabled() ? Promise.resolve(previewAccountState()) : loadProspectusAccountState(returnPath)).then(loaded => {
|
|
35
|
+
if (!cancelled) setSummary(summaryOf(loaded));
|
|
36
|
+
});
|
|
37
|
+
const onChange = (event: Event) => setSummary(summaryOf((event as CustomEvent<ProspectusAccountState>).detail));
|
|
38
|
+
window.addEventListener(ACCOUNT_STATE_EVENT, onChange);
|
|
39
|
+
return () => { cancelled = true; window.removeEventListener(ACCOUNT_STATE_EVENT, onChange); };
|
|
40
|
+
}, []);
|
|
41
|
+
return summary;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The publishing account the signed-in person is acting as. It appears only on sites that have Prospectus
|
|
46
|
+
* accounts configured; a site with its own user pool never shows it. Choosing an account records the choice
|
|
47
|
+
* in the sign-in session and reloads so every page reads the new account.
|
|
48
|
+
*/
|
|
49
|
+
export function ProspectusAccountChooser() {
|
|
50
|
+
const [state, setState] = useState<ProspectusAccountState>({ status: "unavailable" });
|
|
51
|
+
const [error, setError] = useState<string | null>(null);
|
|
52
|
+
const [pending, setPending] = useState<string | null>(null);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
let cancelled = false;
|
|
55
|
+
const returnPath = `${window.location.pathname}${window.location.search}`;
|
|
56
|
+
(isLocalPreviewEnabled() ? Promise.resolve(previewAccountState()) : loadProspectusAccountState(returnPath))
|
|
57
|
+
.then(loaded => { if (!cancelled) setState(loaded); });
|
|
58
|
+
return () => { cancelled = true; };
|
|
59
|
+
}, []);
|
|
60
|
+
if (state.status === "unavailable") return null;
|
|
61
|
+
if (state.status === "signedOut") {
|
|
62
|
+
return <div className="organizer-account-chooser" data-prospectus-account-chooser="signed-out">
|
|
63
|
+
<a className="organizer-account-chooser-link" href={state.signInUrl}>Sign in with Prospectus</a>
|
|
64
|
+
</div>;
|
|
65
|
+
}
|
|
66
|
+
const choose = async (accountId: string) => {
|
|
67
|
+
if (accountId === state.selectedAccountId || pending) return;
|
|
68
|
+
setPending(accountId); setError(null);
|
|
69
|
+
if (isLocalPreviewEnabled()) {
|
|
70
|
+
setLocalPreviewTenantId(accountId);
|
|
71
|
+
window.location.reload();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const result = await selectProspectusAccount(accountId);
|
|
75
|
+
setPending(null);
|
|
76
|
+
if ("error" in result) { setError(result.error); return; }
|
|
77
|
+
window.location.reload();
|
|
78
|
+
};
|
|
79
|
+
return <div className="organizer-account-chooser" data-prospectus-account-chooser="signed-in">
|
|
80
|
+
<p className="organizer-account-chooser-heading">Publishing account</p>
|
|
81
|
+
{state.publishingAccounts.length === 0
|
|
82
|
+
? <p className="organizer-account-chooser-note">No publishing accounts yet.</p>
|
|
83
|
+
: <ul role="listbox" aria-label="Publishing account">
|
|
84
|
+
{state.publishingAccounts.map(account => {
|
|
85
|
+
const selected = account.accountId === state.selectedAccountId;
|
|
86
|
+
return <li key={account.accountId} role="presentation">
|
|
87
|
+
<button aria-selected={selected} data-selected={selected || undefined} disabled={pending !== null} onClick={() => { void choose(account.accountId); }} role="option" type="button">
|
|
88
|
+
<span><strong>{account.name ?? account.accountId}</strong><small>{roleLabel(account.role)}</small></span>
|
|
89
|
+
{selected ? <Check size={14} aria-label="Selected" /> : null}
|
|
90
|
+
</button>
|
|
91
|
+
</li>;
|
|
92
|
+
})}
|
|
93
|
+
</ul>}
|
|
94
|
+
{!state.selectedAccountId && state.publishingAccounts.length > 0 ? <p className="organizer-account-chooser-note">Choose the account you are working as.</p> : null}
|
|
95
|
+
{state.cyclotronLabAccess ? <p className="organizer-account-chooser-note" data-cyclotron-lab-access>Cyclotron lab access is on for you.</p> : null}
|
|
96
|
+
{error ? <p className="organizer-account-chooser-note" role="alert">{error}</p> : null}
|
|
97
|
+
</div>;
|
|
98
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { getContentSource, type ContentSource } from "./site-env";
|
|
3
|
+
import { tenantIdFromEnvironment } from "./tenant-keys";
|
|
3
4
|
|
|
4
5
|
export type ContentSourceContext = {
|
|
5
6
|
source: ContentSource;
|
|
6
7
|
/** Request-scoped data client (drafts mode: carries the visitor's session cookies). */
|
|
7
8
|
client?: unknown;
|
|
9
|
+
/** Tenant of the selected Prospectus account; falls back to PAPYRUS_TENANT_ID. */
|
|
10
|
+
tenantId?: string;
|
|
8
11
|
};
|
|
9
12
|
|
|
10
13
|
const contentSourceStorage = new AsyncLocalStorage<ContentSourceContext>();
|
|
@@ -13,8 +16,13 @@ export function runWithContentSource<T>(
|
|
|
13
16
|
source: ContentSource,
|
|
14
17
|
operation: () => Promise<T>,
|
|
15
18
|
client?: unknown,
|
|
19
|
+
tenantId?: string,
|
|
16
20
|
): Promise<T> {
|
|
17
|
-
return contentSourceStorage.run({ source, client }, operation);
|
|
21
|
+
return contentSourceStorage.run({ source, client, tenantId }, operation);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function currentTenantId(): string | undefined {
|
|
25
|
+
return contentSourceStorage.getStore()?.tenantId ?? tenantIdFromEnvironment();
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
export function currentRequestClient<T>(): T | undefined {
|
|
@@ -20,7 +20,9 @@ import { BodyIrError, matchBodyImages, projectBodyIr, type BodyProjection, type
|
|
|
20
20
|
import { SITE_BRAND } from "./site-brand";
|
|
21
21
|
import { shouldFetchVideoScripts } from "./video-mode";
|
|
22
22
|
import { collectVideoScriptTargets, parseVideoScriptRef, videomlItemSlug, type VideoScriptRef } from "./video-script";
|
|
23
|
-
import {
|
|
23
|
+
import { selectedTenantIdFromCookies } from "./prospectus-tenant";
|
|
24
|
+
import { currentContentSource, currentRequestClient, currentTenantId, runWithContentSource } from "./content-source-context";
|
|
25
|
+
import { rowBelongsToTenant, tenantScopedValue } from "./tenant-keys";
|
|
24
26
|
import { withRateLimitRetry } from "./rate-limit-retry";
|
|
25
27
|
import { getContentSource } from "./site-env";
|
|
26
28
|
|
|
@@ -70,6 +72,7 @@ type GraphQLEditionItem = {
|
|
|
70
72
|
|
|
71
73
|
type GraphQLItem = {
|
|
72
74
|
id: string;
|
|
75
|
+
tenantId?: string | null;
|
|
73
76
|
versionNumber?: number | null;
|
|
74
77
|
type: string;
|
|
75
78
|
status: string;
|
|
@@ -152,9 +155,10 @@ async function withReaderGraphQLContext<T>(operation: () => Promise<T>): Promise
|
|
|
152
155
|
// Drafts are read as the signed-in visitor: the client reads the session from the request cookies, and it is
|
|
153
156
|
// kept in the request's async context (never a module global) so one visitor's session cannot serve another.
|
|
154
157
|
const { cookies } = await import("next/headers");
|
|
155
|
-
await cookies();
|
|
158
|
+
const cookieStore = await cookies();
|
|
156
159
|
const client = generateServerClientUsingCookies<Schema>({ config, cookies, authMode: "userPool" });
|
|
157
|
-
|
|
160
|
+
const selectedTenantId = await selectedTenantIdFromCookies(cookieStore);
|
|
161
|
+
return runWithContentSource(source, operation, client, selectedTenantId);
|
|
158
162
|
}
|
|
159
163
|
return withRateLimitRetry(() =>
|
|
160
164
|
runWithGuestAmplifyContext(() =>
|
|
@@ -587,6 +591,10 @@ type ItemSourceModel = {
|
|
|
587
591
|
listPublishedItemsByTypeStatusAndPublishedAt: (input: { typeStatus: string }, options: ListOptions) => ListResponse<GraphQLItem>;
|
|
588
592
|
};
|
|
589
593
|
|
|
594
|
+
type TenantSlugSourceModel = {
|
|
595
|
+
publishedItemByTenantSlug: (input: { tenantSlug: string }, options: ListOptions) => ListResponse<GraphQLItem>;
|
|
596
|
+
};
|
|
597
|
+
|
|
590
598
|
type MediaAssetSourceModel = {
|
|
591
599
|
listPublishedMediaAssetsByItemAndSortKey: (
|
|
592
600
|
input: { publishedItemId: string },
|
|
@@ -671,20 +679,33 @@ function getPublishedEditionItemModel(): EditionItemSourceModel {
|
|
|
671
679
|
return model as unknown as EditionItemSourceModel;
|
|
672
680
|
}
|
|
673
681
|
|
|
682
|
+
async function getItemWithinTenant(
|
|
683
|
+
tenantId: string | undefined,
|
|
684
|
+
lookup: Promise<GraphQLGetResponse<GraphQLItem>>,
|
|
685
|
+
): Promise<GraphQLGetResponse<GraphQLItem>> {
|
|
686
|
+
const response = await lookup;
|
|
687
|
+
return rowBelongsToTenant(tenantId, response.data) ? response : { ...response, data: null };
|
|
688
|
+
}
|
|
689
|
+
|
|
674
690
|
function getPublishedItemModel(): ItemSourceModel {
|
|
691
|
+
const tenantId = currentTenantId();
|
|
675
692
|
if (currentContentSource() === "drafts") {
|
|
676
693
|
const model = getClient().models.Item;
|
|
677
694
|
if (!model) throw missingProjectionModelError("Item");
|
|
678
695
|
return {
|
|
679
|
-
get: (input, options) =>
|
|
680
|
-
|
|
696
|
+
get: (input, options) =>
|
|
697
|
+
getItemWithinTenant(tenantId, model.get(input, options) as Promise<GraphQLGetResponse<GraphQLItem>>),
|
|
698
|
+
publishedItemBySlug: ({ slug }, options) =>
|
|
699
|
+
(tenantId
|
|
700
|
+
? model.itemByTenantSlug({ tenantSlug: tenantScopedValue(tenantId, slug) }, options)
|
|
701
|
+
: model.itemBySlug({ slug }, options)) as ListResponse<GraphQLItem>,
|
|
681
702
|
listPublishedItemsByTypeStatusAndPublishedAt: ({ typeStatus }, options) => {
|
|
682
703
|
const itemType = typeStatus.split("#")[0];
|
|
683
704
|
return listMergedAcrossStatuses<GraphQLItem>(
|
|
684
705
|
[DRAFT_STATUS, PUBLISHED_STATUS],
|
|
685
706
|
(status, pageOptions) =>
|
|
686
707
|
model.listItemsByTypeStatusAndPublishedAt(
|
|
687
|
-
{ typeStatus: `${itemType}#${status}` },
|
|
708
|
+
{ typeStatus: tenantScopedValue(tenantId, `${itemType}#${status}`) },
|
|
688
709
|
pageOptions,
|
|
689
710
|
) as ListResponse<GraphQLItem>,
|
|
690
711
|
(item) => item.publishedAt ?? "",
|
|
@@ -695,7 +716,21 @@ function getPublishedItemModel(): ItemSourceModel {
|
|
|
695
716
|
}
|
|
696
717
|
const model = getClient().models.PublishedItem;
|
|
697
718
|
if (!model) throw missingProjectionModelError("PublishedItem");
|
|
698
|
-
|
|
719
|
+
const publishedModel = model as unknown as ItemSourceModel;
|
|
720
|
+
if (!tenantId) return publishedModel;
|
|
721
|
+
return {
|
|
722
|
+
get: (input, options) => getItemWithinTenant(tenantId, publishedModel.get(input, options)),
|
|
723
|
+
publishedItemBySlug: ({ slug }, options) =>
|
|
724
|
+
(model as unknown as TenantSlugSourceModel).publishedItemByTenantSlug(
|
|
725
|
+
{ tenantSlug: tenantScopedValue(tenantId, slug) },
|
|
726
|
+
options,
|
|
727
|
+
),
|
|
728
|
+
listPublishedItemsByTypeStatusAndPublishedAt: ({ typeStatus }, options) =>
|
|
729
|
+
publishedModel.listPublishedItemsByTypeStatusAndPublishedAt(
|
|
730
|
+
{ typeStatus: tenantScopedValue(tenantId, typeStatus) },
|
|
731
|
+
options,
|
|
732
|
+
),
|
|
733
|
+
};
|
|
699
734
|
}
|
|
700
735
|
|
|
701
736
|
function getPublishedMediaAssetModel(): MediaAssetSourceModel {
|
|
@@ -4,11 +4,12 @@ import type { MediaAssetRecord } from "./newsroom-media";
|
|
|
4
4
|
import { EMPTY_STORY_REPORTING_TRAIL, type StoryReportingTrail, type TrailVersion } from "./story-reporting-trail";
|
|
5
5
|
import { contentItemStageKeys, type ContentItemVersionRecord } from "./content-item-lifecycle";
|
|
6
6
|
import { getContentItemStageDefinitions } from "./site-brand";
|
|
7
|
+
import { LOCAL_PREVIEW_TENANTS, getLocalPreviewTenantId } from "./local-preview-tenant";
|
|
7
8
|
|
|
8
9
|
const SEED_TIMESTAMP_BASE = Date.parse("2026-10-01T12:00:00.000Z");
|
|
9
10
|
const ONE_HOUR_IN_MILLISECONDS = 60 * 60 * 1000;
|
|
10
11
|
|
|
11
|
-
const
|
|
12
|
+
const SEED_CONTENT_ITEM_HEADLINES_BY_POSITION_FOR_TENANT_ZERO: readonly (readonly string[])[] = [
|
|
12
13
|
["Three sources confirm the zoning reversal", "Reporting packet: school board redistricting"],
|
|
13
14
|
["Draft in progress: the last ferry crossing", "Draft in progress: neighborhood bakery profile"],
|
|
14
15
|
["Illustration brief: flooded streets, aerial view", "Illustration brief: the market at dawn"],
|
|
@@ -18,6 +19,24 @@ const SEED_CONTENT_ITEM_HEADLINES_BY_POSITION: readonly (readonly string[])[] =
|
|
|
18
19
|
["Spiked: the unconfirmed council rumor", "Spiked: the recycled press release"],
|
|
19
20
|
];
|
|
20
21
|
|
|
22
|
+
const SEED_CONTENT_ITEM_HEADLINES_BY_POSITION_FOR_TENANT_ONE: readonly (readonly string[])[] = [
|
|
23
|
+
["Field note: spore dispersal on a damp morning", "Field note: the dung beetle census"],
|
|
24
|
+
["Draft in progress: why fungi throw their caps", "Draft in progress: the compost experiment"],
|
|
25
|
+
["Illustration brief: a cap in flight, close up", "Illustration brief: the mycelium network"],
|
|
26
|
+
["Layout pass: the monthly field guide", "Layout pass: the reader mailbag"],
|
|
27
|
+
["Published: a season of mushroom sightings", "Published: how the forest floor recycles"],
|
|
28
|
+
["Held for spring: the truffle hunt", "Evergreen: a beginner's guide to identification"],
|
|
29
|
+
["Spiked: the unverified foraging claim", "Spiked: the duplicate field report"],
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const SEED_CONTENT_ITEM_HEADLINES_BY_TENANT = [SEED_CONTENT_ITEM_HEADLINES_BY_POSITION_FOR_TENANT_ZERO, SEED_CONTENT_ITEM_HEADLINES_BY_POSITION_FOR_TENANT_ONE] as const;
|
|
33
|
+
|
|
34
|
+
function headlinesForSelectedTenant(): readonly (readonly string[])[] {
|
|
35
|
+
const tenantId = getLocalPreviewTenantId();
|
|
36
|
+
const tenant = LOCAL_PREVIEW_TENANTS.find(candidate => candidate.accountId === tenantId) ?? LOCAL_PREVIEW_TENANTS[0];
|
|
37
|
+
return SEED_CONTENT_ITEM_HEADLINES_BY_TENANT[tenant.fixtureHeadlineSet] ?? SEED_CONTENT_ITEM_HEADLINES_BY_TENANT[0];
|
|
38
|
+
}
|
|
39
|
+
|
|
21
40
|
function isoTimestampHoursBeforeBase(hours: number): string {
|
|
22
41
|
return new Date(SEED_TIMESTAMP_BASE - hours * ONE_HOUR_IN_MILLISECONDS).toISOString();
|
|
23
42
|
}
|
|
@@ -29,7 +48,8 @@ function slugFromHeadline(headline: string): string {
|
|
|
29
48
|
export function createLocalPreviewContentItemVersions(): ContentItemVersionRecord[] {
|
|
30
49
|
const stageKeys = contentItemStageKeys(getContentItemStageDefinitions());
|
|
31
50
|
return stageKeys.flatMap((stageKey, stageIndex) => {
|
|
32
|
-
const
|
|
51
|
+
const tenantHeadlines = headlinesForSelectedTenant();
|
|
52
|
+
const headlines = tenantHeadlines[stageIndex % tenantHeadlines.length];
|
|
33
53
|
return headlines.map((headline, headlineIndex) => {
|
|
34
54
|
const slug = slugFromHeadline(headline);
|
|
35
55
|
return {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local preview stands in for the multi-tenant newsroom: choosing a publishing account in the account menu
|
|
3
|
+
* switches which publication's content the preview shows. The choice is a cookie so server and browser agree.
|
|
4
|
+
*/
|
|
5
|
+
export const LOCAL_PREVIEW_TENANT_COOKIE = "papyrus-preview-tenant";
|
|
6
|
+
export const LOCAL_PREVIEW_DEFAULT_TENANT_ID = "preview-papyrus";
|
|
7
|
+
|
|
8
|
+
export type LocalPreviewTenant = { accountId: string; name: string; role: string; fixtureHeadlineSet: number };
|
|
9
|
+
|
|
10
|
+
export const LOCAL_PREVIEW_TENANTS: readonly LocalPreviewTenant[] = [
|
|
11
|
+
{ accountId: LOCAL_PREVIEW_DEFAULT_TENANT_ID, name: "Papyrus", role: "publisher", fixtureHeadlineSet: 0 },
|
|
12
|
+
{ accountId: "preview-pilobolus", name: "Pilobolus", role: "editor", fixtureHeadlineSet: 1 },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export function previewTenantIdFromCookieHeader(cookieHeader: string | null | undefined): string {
|
|
16
|
+
const match = cookieHeader?.split(";").map(part => part.trim()).find(part => part.startsWith(`${LOCAL_PREVIEW_TENANT_COOKIE}=`));
|
|
17
|
+
const value = match ? decodeURIComponent(match.slice(LOCAL_PREVIEW_TENANT_COOKIE.length + 1)) : "";
|
|
18
|
+
return LOCAL_PREVIEW_TENANTS.some(tenant => tenant.accountId === value) ? value : LOCAL_PREVIEW_DEFAULT_TENANT_ID;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getLocalPreviewTenantId(): string {
|
|
22
|
+
return typeof document === "undefined" ? LOCAL_PREVIEW_DEFAULT_TENANT_ID : previewTenantIdFromCookieHeader(document.cookie);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function setLocalPreviewTenantId(accountId: string): void {
|
|
26
|
+
document.cookie = `${LOCAL_PREVIEW_TENANT_COOKIE}=${encodeURIComponent(accountId)}; path=/; max-age=31536000; samesite=lax`;
|
|
27
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PROSPECTUS_SESSION_COOKIE,
|
|
3
|
+
loadProspectusSessionSealingSecret,
|
|
4
|
+
openProspectusCookie,
|
|
5
|
+
type ProspectusSignedInSession,
|
|
6
|
+
} from "./prospectus-session";
|
|
7
|
+
import { tenantIdForProspectusAccount } from "./tenant-keys";
|
|
8
|
+
|
|
9
|
+
type CookieReader = { get(name: string): { value: string } | undefined };
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The tenant of the publishing account the signed-in person selected, read from the sealed Prospectus session
|
|
13
|
+
* cookie. Returns undefined on a site without Prospectus, with no session, or with no account selected, so the
|
|
14
|
+
* caller falls back to the site's own tenant (PAPYRUS_TENANT_ID).
|
|
15
|
+
*/
|
|
16
|
+
export async function selectedTenantIdFromCookies(cookies: CookieReader, loadSecret: () => Promise<string | null> = loadProspectusSessionSealingSecret): Promise<string | undefined> {
|
|
17
|
+
const sealingSecret = await loadSecret();
|
|
18
|
+
if (!sealingSecret) return undefined;
|
|
19
|
+
const session = openProspectusCookie<ProspectusSignedInSession>(sealingSecret, PROSPECTUS_SESSION_COOKIE, cookies.get(PROSPECTUS_SESSION_COOKIE)?.value);
|
|
20
|
+
if (!session || session.accessTokenExpiresAt * 1000 <= Date.now()) return undefined;
|
|
21
|
+
return tenantIdForProspectusAccount(session.selectedAccountId);
|
|
22
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const TENANT_ID_ENVIRONMENT_VARIABLE = "PAPYRUS_TENANT_ID";
|
|
2
|
+
const TENANT_KEY_SEPARATOR = "#";
|
|
3
|
+
const TENANT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
4
|
+
|
|
5
|
+
export function validateTenantId(tenantId: string): string {
|
|
6
|
+
if (!TENANT_ID_PATTERN.test(tenantId)) {
|
|
7
|
+
throw new Error(`Tenant id "${tenantId}" must be lowercase letters, digits and hyphens, starting with a letter or digit.`);
|
|
8
|
+
}
|
|
9
|
+
return tenantId;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function tenantIdFromEnvironment(environment: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
13
|
+
const configured = environment[TENANT_ID_ENVIRONMENT_VARIABLE]?.trim();
|
|
14
|
+
return configured ? validateTenantId(configured) : undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function tenantScopedValue(tenantId: string | undefined, value: string): string {
|
|
18
|
+
if (!tenantId) return value;
|
|
19
|
+
const prefix = `${tenantId}${TENANT_KEY_SEPARATOR}`;
|
|
20
|
+
return value.startsWith(prefix) ? value : `${prefix}${value}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function rowBelongsToTenant(tenantId: string | undefined, row: { tenantId?: string | null } | null | undefined): boolean {
|
|
24
|
+
if (!tenantId || !row) return true;
|
|
25
|
+
return row.tenantId === tenantId;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The tenant a Prospectus publishing account maps to: its id as lowercase letters, digits and hyphens. */
|
|
29
|
+
export function tenantIdForProspectusAccount(accountId: string | null | undefined): string | undefined {
|
|
30
|
+
const normalized = (accountId ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
|
|
31
|
+
return normalized ? validateTenantId(normalized) : undefined;
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -101,6 +101,7 @@
|
|
|
101
101
|
"./components/presentation-header": "./components/presentation-header.tsx",
|
|
102
102
|
"./components/presentation-rhythm-hrule": "./components/presentation-rhythm-hrule.tsx",
|
|
103
103
|
"./components/priming-proof-page": "./components/priming-proof-page.tsx",
|
|
104
|
+
"./components/prospectus-account-chooser": "./components/prospectus-account-chooser.tsx",
|
|
104
105
|
"./components/publication-cyclotron-panel": "./components/publication-cyclotron-panel.tsx",
|
|
105
106
|
"./components/reader-auth-control": "./components/reader-auth-control.tsx",
|
|
106
107
|
"./components/reader-auth-state": "./components/reader-auth-state.ts",
|
|
@@ -191,6 +192,7 @@
|
|
|
191
192
|
"./lib/layout-types": "./lib/layout-types.ts",
|
|
192
193
|
"./lib/layout-warnings": "./lib/layout-warnings.ts",
|
|
193
194
|
"./lib/local-preview-seed": "./lib/local-preview-seed.ts",
|
|
195
|
+
"./lib/local-preview-tenant": "./lib/local-preview-tenant.ts",
|
|
194
196
|
"./lib/local-preview": "./lib/local-preview.ts",
|
|
195
197
|
"./lib/markus-body": "./lib/markus-body.ts",
|
|
196
198
|
"./lib/markus-ir": "./lib/markus-ir.ts",
|
|
@@ -233,6 +235,7 @@
|
|
|
233
235
|
"./lib/prospectus-session": "./lib/prospectus-session.ts",
|
|
234
236
|
"./lib/prospectus-sign-in-routes": "./lib/prospectus-sign-in-routes.ts",
|
|
235
237
|
"./lib/prospectus-site-identity": "./lib/prospectus-site-identity.ts",
|
|
238
|
+
"./lib/prospectus-tenant": "./lib/prospectus-tenant.ts",
|
|
236
239
|
"./lib/public-placeholder-config": "./lib/public-placeholder-config.ts",
|
|
237
240
|
"./lib/publication-cyclotron": "./lib/publication-cyclotron.ts",
|
|
238
241
|
"./lib/publication-items": "./lib/publication-items.ts",
|
|
@@ -266,6 +269,7 @@
|
|
|
266
269
|
"./lib/staging-gated-path": "./lib/staging-gated-path.ts",
|
|
267
270
|
"./lib/staging-preview-object": "./lib/staging-preview-object.ts",
|
|
268
271
|
"./lib/story-reporting-trail": "./lib/story-reporting-trail.ts",
|
|
272
|
+
"./lib/tenant-keys": "./lib/tenant-keys.ts",
|
|
269
273
|
"./lib/test-editor-override": "./lib/test-editor-override.ts",
|
|
270
274
|
"./lib/text-columns": "./lib/text-columns.ts",
|
|
271
275
|
"./lib/themed-image": "./lib/themed-image.ts",
|
|
@@ -364,7 +368,7 @@
|
|
|
364
368
|
"optional": true
|
|
365
369
|
}
|
|
366
370
|
},
|
|
367
|
-
"version": "1.4.0-next.
|
|
371
|
+
"version": "1.4.0-next.6",
|
|
368
372
|
"dependencies": {
|
|
369
373
|
"@aws-amplify/adapter-nextjs": "^1.7.3",
|
|
370
374
|
"@aws-crypto/sha256-js": "5.2.0",
|
package/routes.manifest.json
CHANGED