@aglyn/plugins-marketplace 1.0.0-beta.291 → 1.0.0-beta.293
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 +14 -14
- package/src/lib/components/listing-detail-editor.component.js +32 -10
- package/src/lib/components/listing-detail-editor.component.js.map +1 -1
- package/src/lib/components/listing-verification-request.component.js +32 -11
- package/src/lib/components/listing-verification-request.component.js.map +1 -1
- package/src/lib/components/org-seller-panel.component.js +121 -143
- package/src/lib/components/org-seller-panel.component.js.map +1 -1
- package/src/lib/components/publish-artifact-dialog.component.js +115 -115
- package/src/lib/components/publish-artifact-dialog.component.js.map +1 -1
- package/src/lib/components/publish-plugin-form.component.js +32 -36
- package/src/lib/components/publish-plugin-form.component.js.map +1 -1
- package/src/lib/components/publisher-agreement-checkbox.component.d.ts +56 -0
- package/src/lib/components/publisher-agreement-checkbox.component.js +121 -0
- package/src/lib/components/publisher-agreement-checkbox.component.js.map +1 -0
- package/src/lib/components/publisher-agreement-confirm-dialog.component.d.ts +45 -0
- package/src/lib/components/publisher-agreement-confirm-dialog.component.js +109 -0
- package/src/lib/components/publisher-agreement-confirm-dialog.component.js.map +1 -0
- package/src/lib/components/publisher-agreement-summary.component.d.ts +4 -3
- package/src/lib/components/publisher-agreement-summary.component.js +4 -3
- package/src/lib/components/publisher-agreement-summary.component.js.map +1 -1
- package/src/lib/components/starter-picker-shelf.component.d.ts +29 -0
- package/src/lib/components/starter-picker-shelf.component.js +317 -0
- package/src/lib/components/starter-picker-shelf.component.js.map +1 -0
- package/src/lib/declarations.server.js +2 -1
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/hooks/use-starter-purchase.d.ts +31 -0
- package/src/lib/hooks/use-starter-purchase.js +101 -0
- package/src/lib/hooks/use-starter-purchase.js.map +1 -0
- package/src/lib/hooks/use-starter-search.d.ts +42 -0
- package/src/lib/hooks/use-starter-search.js +137 -0
- package/src/lib/hooks/use-starter-search.js.map +1 -0
- package/src/lib/model/listing-visibility.d.ts +17 -9
- package/src/lib/model/listing-visibility.js +18 -32
- package/src/lib/model/listing-visibility.js.map +1 -1
- package/src/lib/model/publisher-agreement.d.ts +105 -8
- package/src/lib/model/publisher-agreement.js +131 -24
- package/src/lib/model/publisher-agreement.js.map +1 -1
- package/src/lib/model/site-starter-listing.js +4 -1
- package/src/lib/model/site-starter-listing.js.map +1 -1
- package/src/lib/model/starter-query.d.ts +159 -0
- package/src/lib/model/starter-query.js +267 -0
- package/src/lib/model/starter-query.js.map +1 -0
- package/src/lib/plugin.js +12 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/checkout.js +45 -12
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/preview-image.js +38 -8
- package/src/lib/server/preview-image.js.map +1 -1
- package/src/lib/server/publish-dataset-schema.js +27 -2
- package/src/lib/server/publish-dataset-schema.js.map +1 -1
- package/src/lib/server/publish-email-starter.js +27 -2
- package/src/lib/server/publish-email-starter.js.map +1 -1
- package/src/lib/server/publish-email-template.js +27 -2
- package/src/lib/server/publish-email-template.js.map +1 -1
- package/src/lib/server/publish-layout.js +27 -2
- package/src/lib/server/publish-layout.js.map +1 -1
- package/src/lib/server/publish-plugin.js +120 -14
- package/src/lib/server/publish-plugin.js.map +1 -1
- package/src/lib/server/publish-preconditions.d.ts +13 -9
- package/src/lib/server/publish-preconditions.js +19 -45
- package/src/lib/server/publish-preconditions.js.map +1 -1
- package/src/lib/server/publish-starter.js +27 -2
- package/src/lib/server/publish-starter.js.map +1 -1
- package/src/lib/server/publish-template.js +27 -2
- package/src/lib/server/publish-template.js.map +1 -1
- package/src/lib/server/publish-theme.js +27 -2
- package/src/lib/server/publish-theme.js.map +1 -1
- package/src/lib/server/publish.js +27 -2
- package/src/lib/server/publish.js.map +1 -1
- package/src/lib/server/publisher-agreement-acceptance.d.ts +49 -0
- package/src/lib/server/publisher-agreement-acceptance.js +97 -0
- package/src/lib/server/publisher-agreement-acceptance.js.map +1 -0
- package/src/lib/server/publisher-profile-save.js +46 -97
- package/src/lib/server/publisher-profile-save.js.map +1 -1
- package/src/lib/server/site-starter-source.js +97 -25
- package/src/lib/server/site-starter-source.js.map +1 -1
- package/src/lib/server/starters.d.ts +31 -0
- package/src/lib/server/starters.js +170 -0
- package/src/lib/server/starters.js.map +1 -0
- package/src/lib/server/verification-request.js +33 -8
- package/src/lib/server/verification-request.js.map +1 -1
- package/src/lib/server.js +14 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/components/publisher-agreement-client.d.ts +0 -48
- package/src/lib/components/publisher-agreement-client.js +0 -93
- package/src/lib/components/publisher-agreement-client.js.map +0 -1
- package/src/lib/components/publisher-agreement-dialog.component.d.ts +0 -47
- package/src/lib/components/publisher-agreement-dialog.component.js +0 -222
- package/src/lib/components/publisher-agreement-dialog.component.js.map +0 -1
- package/src/lib/components/use-publisher-agreement-gate.d.ts +0 -58
- package/src/lib/components/use-publisher-agreement-gate.js +0 -166
- package/src/lib/components/use-publisher-agreement-gate.js.map +0 -1
- package/src/lib/model/publisher-agreement-refusal.d.ts +0 -46
- package/src/lib/model/publisher-agreement-refusal.js +0 -41
- package/src/lib/model/publisher-agreement-refusal.js.map +0 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** How one "Buy and continue" ended. */
|
|
2
|
+
export type StarterPurchaseOutcome =
|
|
3
|
+
/** The payment page is opening: the person comes back to Create site on the same pick. */
|
|
4
|
+
'opening'
|
|
5
|
+
/** The workspace owns it already: nothing to pay, the site can be created. */
|
|
6
|
+
| 'owned'
|
|
7
|
+
/** It could not open; {@link StarterPurchase.error} says why. */
|
|
8
|
+
| 'failed';
|
|
9
|
+
export interface StarterPurchase {
|
|
10
|
+
buy: (listingId: string) => Promise<StarterPurchaseOutcome>;
|
|
11
|
+
/** A purchase is opening. */
|
|
12
|
+
busy: boolean;
|
|
13
|
+
/** Why the last one did not open, or `null`. */
|
|
14
|
+
error: string | null;
|
|
15
|
+
}
|
|
16
|
+
/** Where the browser goes to pay: replaced in a spec. */
|
|
17
|
+
export declare const starterPurchaseNavigation: {
|
|
18
|
+
assign: (url: string) => void;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* "Buy and continue" for a priced starter in Create site (AGL-4049): opens
|
|
22
|
+
* the marketplace's payment for it, licensed to the workspace the site is
|
|
23
|
+
* for, and names Create site as the way back, so the person returns to the
|
|
24
|
+
* same pick with the site still to be created. Today's payment page, until
|
|
25
|
+
* the marketplace pays in-app.
|
|
26
|
+
*
|
|
27
|
+
* One attempt per starter: the same key goes with a second click, so a
|
|
28
|
+
* double click cannot open a second payment (AGL-1697). A workspace that
|
|
29
|
+
* already owns it is told so, not charged again.
|
|
30
|
+
*/
|
|
31
|
+
export declare function useStarterPurchase(orgId: string | undefined): StarterPurchase;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { lockdownRefusalText, parseLockdownRefusal } from "@aglyn/aglyn/app-utils/lockdown";
|
|
18
|
+
import { readGaClientId } from "@aglyn/aglyn/app-utils/analytics-events";
|
|
19
|
+
import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
20
|
+
import { useUser } from "@aglyn/tenant-feature-instance";
|
|
21
|
+
import { useCallback, useRef, useState } from "react";
|
|
22
|
+
import { STARTER_CHECKOUT_PATH, starterCheckoutBody } from "../model/starter-query.js";
|
|
23
|
+
/** Where the browser goes to pay: replaced in a spec. */ export const starterPurchaseNavigation = {
|
|
24
|
+
assign: (url)=>window.location.assign(url)
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* "Buy and continue" for a priced starter in Create site (AGL-4049): opens
|
|
28
|
+
* the marketplace's payment for it, licensed to the workspace the site is
|
|
29
|
+
* for, and names Create site as the way back, so the person returns to the
|
|
30
|
+
* same pick with the site still to be created. Today's payment page, until
|
|
31
|
+
* the marketplace pays in-app.
|
|
32
|
+
*
|
|
33
|
+
* One attempt per starter: the same key goes with a second click, so a
|
|
34
|
+
* double click cannot open a second payment (AGL-1697). A workspace that
|
|
35
|
+
* already owns it is told so, not charged again.
|
|
36
|
+
*/ export function useStarterPurchase(orgId) {
|
|
37
|
+
const { data: user } = useUser();
|
|
38
|
+
const userRef = useRef(user);
|
|
39
|
+
userRef.current = user;
|
|
40
|
+
const attempts = useRef(new Map());
|
|
41
|
+
const [busy, setBusy] = useState(false);
|
|
42
|
+
const [error, setError] = useState(null);
|
|
43
|
+
const buy = useCallback(async (listingId)=>{
|
|
44
|
+
var _attempts_current_get;
|
|
45
|
+
if (!orgId || !listingId) return 'failed';
|
|
46
|
+
const key = (_attempts_current_get = attempts.current.get(listingId)) != null ? _attempts_current_get : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
47
|
+
attempts.current.set(listingId, key);
|
|
48
|
+
setBusy(true);
|
|
49
|
+
setError(null);
|
|
50
|
+
// Busy until the page leaves for the payment, so a second click cannot
|
|
51
|
+
// land in between.
|
|
52
|
+
let opening = false;
|
|
53
|
+
try {
|
|
54
|
+
const response = await authorizedFetch(userRef.current, STARTER_CHECKOUT_PATH, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: {
|
|
57
|
+
'Content-Type': 'application/json',
|
|
58
|
+
'Idempotency-Key': key
|
|
59
|
+
},
|
|
60
|
+
body: JSON.stringify(starterCheckoutBody({
|
|
61
|
+
listingId,
|
|
62
|
+
orgId,
|
|
63
|
+
gaClientId: await readGaClientId(process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID)
|
|
64
|
+
}))
|
|
65
|
+
});
|
|
66
|
+
const payload = await response.json().catch(()=>null);
|
|
67
|
+
if (response.status === 409 && (payload == null ? void 0 : payload.code) === 'already_purchased') return 'owned';
|
|
68
|
+
const locked = parseLockdownRefusal(response.status, payload);
|
|
69
|
+
if (locked) {
|
|
70
|
+
setError(lockdownRefusalText(locked));
|
|
71
|
+
return 'failed';
|
|
72
|
+
}
|
|
73
|
+
if (response.status === 501) {
|
|
74
|
+
setError('Purchases are not set up here yet, so a starter with a price cannot be bought.');
|
|
75
|
+
return 'failed';
|
|
76
|
+
}
|
|
77
|
+
if (!response.ok || typeof (payload == null ? void 0 : payload.url) !== 'string' || !payload.url) {
|
|
78
|
+
setError(typeof (payload == null ? void 0 : payload.error) === 'string' && payload.error ? payload.error : 'The payment could not open. Try again.');
|
|
79
|
+
return 'failed';
|
|
80
|
+
}
|
|
81
|
+
opening = true;
|
|
82
|
+
starterPurchaseNavigation.assign(payload.url);
|
|
83
|
+
return 'opening';
|
|
84
|
+
} catch (caught) {
|
|
85
|
+
console.error(caught);
|
|
86
|
+
setError('The payment could not open. Check your connection and try again.');
|
|
87
|
+
return 'failed';
|
|
88
|
+
} finally{
|
|
89
|
+
if (!opening) setBusy(false);
|
|
90
|
+
}
|
|
91
|
+
}, [
|
|
92
|
+
orgId
|
|
93
|
+
]);
|
|
94
|
+
return {
|
|
95
|
+
buy,
|
|
96
|
+
busy,
|
|
97
|
+
error
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
//# sourceMappingURL=use-starter-purchase.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/hooks/use-starter-purchase.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn/app-utils/lockdown'\nimport { readGaClientId } from '@aglyn/aglyn/app-utils/analytics-events'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { useCallback, useRef, useState } from 'react'\nimport { STARTER_CHECKOUT_PATH, starterCheckoutBody } from '../model/starter-query'\n\n/** How one \"Buy and continue\" ended. */\nexport type StarterPurchaseOutcome =\n /** The payment page is opening: the person comes back to Create site on the same pick. */\n | 'opening'\n /** The workspace owns it already: nothing to pay, the site can be created. */\n | 'owned'\n /** It could not open; {@link StarterPurchase.error} says why. */\n | 'failed'\n\nexport interface StarterPurchase {\n buy: (listingId: string) => Promise<StarterPurchaseOutcome>\n /** A purchase is opening. */\n busy: boolean\n /** Why the last one did not open, or `null`. */\n error: string | null\n}\n\n/** Where the browser goes to pay: replaced in a spec. */\nexport const starterPurchaseNavigation = {\n assign: (url: string) => window.location.assign(url),\n}\n\n/**\n * \"Buy and continue\" for a priced starter in Create site (AGL-4049): opens\n * the marketplace's payment for it, licensed to the workspace the site is\n * for, and names Create site as the way back, so the person returns to the\n * same pick with the site still to be created. Today's payment page, until\n * the marketplace pays in-app.\n *\n * One attempt per starter: the same key goes with a second click, so a\n * double click cannot open a second payment (AGL-1697). A workspace that\n * already owns it is told so, not charged again.\n */\nexport function useStarterPurchase(orgId: string | undefined): StarterPurchase {\n const { data: user } = useUser()\n const userRef = useRef(user)\n userRef.current = user\n const attempts = useRef(new Map<string, string>())\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const buy = useCallback(\n async (listingId: string): Promise<StarterPurchaseOutcome> => {\n if (!orgId || !listingId) return 'failed'\n const key =\n attempts.current.get(listingId) ??\n `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`\n attempts.current.set(listingId, key)\n setBusy(true)\n setError(null)\n // Busy until the page leaves for the payment, so a second click cannot\n // land in between.\n let opening = false\n try {\n const response = await authorizedFetch(userRef.current, STARTER_CHECKOUT_PATH, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Idempotency-Key': key },\n body: JSON.stringify(\n starterCheckoutBody({\n listingId,\n orgId,\n gaClientId: await readGaClientId(process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID),\n }),\n ),\n })\n const payload = (await response.json().catch((): null => null)) as\n | { url?: unknown; error?: unknown; code?: unknown }\n | null\n if (response.status === 409 && payload?.code === 'already_purchased') return 'owned'\n const locked = parseLockdownRefusal(response.status, payload)\n if (locked) {\n setError(lockdownRefusalText(locked))\n return 'failed'\n }\n if (response.status === 501) {\n setError('Purchases are not set up here yet, so a starter with a price cannot be bought.')\n return 'failed'\n }\n if (!response.ok || typeof payload?.url !== 'string' || !payload.url) {\n setError(typeof payload?.error === 'string' && payload.error ? payload.error : 'The payment could not open. Try again.')\n return 'failed'\n }\n opening = true\n starterPurchaseNavigation.assign(payload.url)\n return 'opening'\n } catch (caught) {\n console.error(caught)\n setError('The payment could not open. Check your connection and try again.')\n return 'failed'\n } finally {\n if (!opening) setBusy(false)\n }\n },\n [orgId],\n )\n\n return { buy, busy, error }\n}\n"],"names":["lockdownRefusalText","parseLockdownRefusal","readGaClientId","authorizedFetch","useUser","useCallback","useRef","useState","STARTER_CHECKOUT_PATH","starterCheckoutBody","starterPurchaseNavigation","assign","url","window","location","useStarterPurchase","orgId","data","user","userRef","current","attempts","Map","busy","setBusy","error","setError","buy","listingId","key","get","Date","now","toString","Math","random","slice","set","opening","response","method","headers","body","JSON","stringify","gaClientId","process","env","NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID","payload","json","catch","status","code","locked","ok","caught","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAEA,SAASA,mBAAmB,EAAEC,oBAAoB,QAAQ,kCAAiC;AAC3F,SAASC,cAAc,QAAQ,0CAAyC;AACxE,SAASC,eAAe,QAAQ,2CAA0C;AAC1E,SAASC,OAAO,QAAQ,iCAAgC;AACxD,SAASC,WAAW,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAO;AACrD,SAASC,qBAAqB,EAAEC,mBAAmB,QAAQ,4BAAwB;AAmBnF,uDAAuD,GACvD,OAAO,MAAMC,4BAA4B;IACvCC,QAAQ,CAACC,MAAgBC,OAAOC,QAAQ,CAACH,MAAM,CAACC;AAClD,EAAC;AAED;;;;;;;;;;CAUC,GACD,OAAO,SAASG,mBAAmBC,KAAyB;IAC1D,MAAM,EAAEC,MAAMC,IAAI,EAAE,GAAGd;IACvB,MAAMe,UAAUb,OAAOY;IACvBC,QAAQC,OAAO,GAAGF;IAClB,MAAMG,WAAWf,OAAO,IAAIgB;IAC5B,MAAM,CAACC,MAAMC,QAAQ,GAAGjB,SAAS;IACjC,MAAM,CAACkB,OAAOC,SAAS,GAAGnB,SAAwB;IAElD,MAAMoB,MAAMtB,YACV,OAAOuB;YAGHP;QAFF,IAAI,CAACL,SAAS,CAACY,WAAW,OAAO;QACjC,MAAMC,OACJR,wBAAAA,SAASD,OAAO,CAACU,GAAG,CAACF,sBAArBP,wBACA,GAAGU,KAAKC,GAAG,GAAGC,QAAQ,CAAC,IAAI,CAAC,EAAEC,KAAKC,MAAM,GAAGF,QAAQ,CAAC,IAAIG,KAAK,CAAC,GAAG,KAAK;QACzEf,SAASD,OAAO,CAACiB,GAAG,CAACT,WAAWC;QAChCL,QAAQ;QACRE,SAAS;QACT,uEAAuE;QACvE,mBAAmB;QACnB,IAAIY,UAAU;QACd,IAAI;YACF,MAAMC,WAAW,MAAMpC,gBAAgBgB,QAAQC,OAAO,EAAEZ,uBAAuB;gBAC7EgC,QAAQ;gBACRC,SAAS;oBAAE,gBAAgB;oBAAoB,mBAAmBZ;gBAAI;gBACtEa,MAAMC,KAAKC,SAAS,CAClBnC,oBAAoB;oBAClBmB;oBACAZ;oBACA6B,YAAY,MAAM3C,eAAe4C,QAAQC,GAAG,CAACC,mCAAmC;gBAClF;YAEJ;YACA,MAAMC,UAAW,MAAMV,SAASW,IAAI,GAAGC,KAAK,CAAC,IAAY;YAGzD,IAAIZ,SAASa,MAAM,KAAK,OAAOH,CAAAA,2BAAAA,QAASI,IAAI,MAAK,qBAAqB,OAAO;YAC7E,MAAMC,SAASrD,qBAAqBsC,SAASa,MAAM,EAAEH;YACrD,IAAIK,QAAQ;gBACV5B,SAAS1B,oBAAoBsD;gBAC7B,OAAO;YACT;YACA,IAAIf,SAASa,MAAM,KAAK,KAAK;gBAC3B1B,SAAS;gBACT,OAAO;YACT;YACA,IAAI,CAACa,SAASgB,EAAE,IAAI,QAAON,2BAAAA,QAASrC,GAAG,MAAK,YAAY,CAACqC,QAAQrC,GAAG,EAAE;gBACpEc,SAAS,QAAOuB,2BAAAA,QAASxB,KAAK,MAAK,YAAYwB,QAAQxB,KAAK,GAAGwB,QAAQxB,KAAK,GAAG;gBAC/E,OAAO;YACT;YACAa,UAAU;YACV5B,0BAA0BC,MAAM,CAACsC,QAAQrC,GAAG;YAC5C,OAAO;QACT,EAAE,OAAO4C,QAAQ;YACfC,QAAQhC,KAAK,CAAC+B;YACd9B,SAAS;YACT,OAAO;QACT,SAAU;YACR,IAAI,CAACY,SAASd,QAAQ;QACxB;IACF,GACA;QAACR;KAAM;IAGT,OAAO;QAAEW;QAAKJ;QAAME;IAAM;AAC5B"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { SiteStarterFacets, SiteStarterMode } from '@aglyn/aglyn/app-utils/site-starter';
|
|
2
|
+
import type { SiteStarterSummary } from '@aglyn/aglyn/plugin-manager/plugin-site-starters';
|
|
3
|
+
import { type StarterSearchLabels } from '../model/starter-query';
|
|
4
|
+
/** How long the search waits for typing to stop before it asks, as the review queue's does. */
|
|
5
|
+
export declare const STARTER_SEARCH_SETTLE_MS = 300;
|
|
6
|
+
/** Why a search could not be answered: the words to show, and the status that said so (0 for none). */
|
|
7
|
+
export interface StarterSearchError {
|
|
8
|
+
message: string;
|
|
9
|
+
status: number;
|
|
10
|
+
}
|
|
11
|
+
export interface StarterSearchInput {
|
|
12
|
+
/** The workspace the site will belong to; nothing is asked while it resolves. */
|
|
13
|
+
orgId: string | undefined;
|
|
14
|
+
mode: SiteStarterMode;
|
|
15
|
+
/** The person's words, as typed: asked once typing settles. */
|
|
16
|
+
word: string;
|
|
17
|
+
filter?: Partial<SiteStarterFacets>;
|
|
18
|
+
limit?: number;
|
|
19
|
+
/** A listing the answer carries whatever the search finds: a pick being restored (AGL-4049). */
|
|
20
|
+
include?: string | null;
|
|
21
|
+
}
|
|
22
|
+
export interface StarterSearchState {
|
|
23
|
+
/** The starters the last answer listed, best first. */
|
|
24
|
+
rows: SiteStarterSummary[];
|
|
25
|
+
labels: StarterSearchLabels;
|
|
26
|
+
/** A request is out. */
|
|
27
|
+
loading: boolean;
|
|
28
|
+
/** An answer has arrived for this workspace, listing starters or refusing. */
|
|
29
|
+
settled: boolean;
|
|
30
|
+
/** Why the last search was not answered, or `null`. */
|
|
31
|
+
error: StarterSearchError | null;
|
|
32
|
+
/** Asks the same search again. */
|
|
33
|
+
refresh: () => void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The starters a search finds, for "Choose your starter" (AGL-4049): asks
|
|
37
|
+
* `GET /api/marketplace/starters` with the person's own token, once typing
|
|
38
|
+
* has settled, and again whenever the workspace, the mode or a filter
|
|
39
|
+
* changes. Only the newest answer is kept: a slow answer to an older search
|
|
40
|
+
* never replaces a newer one.
|
|
41
|
+
*/
|
|
42
|
+
export declare function useStarterSearch({ orgId, mode, word, filter, limit, include, }: StarterSearchInput): StarterSearchState;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { lockdownRefusalText, parseLockdownRefusal } from "@aglyn/aglyn/app-utils/lockdown";
|
|
18
|
+
import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
19
|
+
import { useUser } from "@aglyn/tenant-feature-instance";
|
|
20
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
21
|
+
import { parseStarterSearchAnswer, STARTER_SEARCH_PATH, starterQueryParams } from "../model/starter-query.js";
|
|
22
|
+
/** How long the search waits for typing to stop before it asks, as the review queue's does. */ export const STARTER_SEARCH_SETTLE_MS = 300;
|
|
23
|
+
const NO_LABELS = {
|
|
24
|
+
base: {},
|
|
25
|
+
fonts: {}
|
|
26
|
+
};
|
|
27
|
+
/** The words a refused or failed search shows. */ function refusalMessage(status, payload) {
|
|
28
|
+
// A lockdown is not a broken search (AGL-1532): it says what is paused.
|
|
29
|
+
const locked = parseLockdownRefusal(status, payload);
|
|
30
|
+
if (locked) return lockdownRefusalText(locked);
|
|
31
|
+
const error = payload == null ? void 0 : payload.error;
|
|
32
|
+
return typeof error === 'string' && error ? error : `Starters could not be loaded (${status})`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The starters a search finds, for "Choose your starter" (AGL-4049): asks
|
|
36
|
+
* `GET /api/marketplace/starters` with the person's own token, once typing
|
|
37
|
+
* has settled, and again whenever the workspace, the mode or a filter
|
|
38
|
+
* changes. Only the newest answer is kept: a slow answer to an older search
|
|
39
|
+
* never replaces a newer one.
|
|
40
|
+
*/ export function useStarterSearch({ orgId, mode, word, filter, limit, include }) {
|
|
41
|
+
var _ref;
|
|
42
|
+
const { data: user } = useUser();
|
|
43
|
+
// The token is read from the user at the moment of asking; the request is
|
|
44
|
+
// keyed on WHO is asking, so a re-render handing over the same account does
|
|
45
|
+
// not ask again.
|
|
46
|
+
const userRef = useRef(user);
|
|
47
|
+
userRef.current = user;
|
|
48
|
+
const uid = (_ref = user == null ? void 0 : user.uid) != null ? _ref : null;
|
|
49
|
+
const [asked, setAsked] = useState(()=>word.trim());
|
|
50
|
+
useEffect(()=>{
|
|
51
|
+
const settledWord = word.trim();
|
|
52
|
+
if (settledWord === asked) return undefined;
|
|
53
|
+
const timer = setTimeout(()=>setAsked(settledWord), STARTER_SEARCH_SETTLE_MS);
|
|
54
|
+
return ()=>clearTimeout(timer);
|
|
55
|
+
}, [
|
|
56
|
+
word,
|
|
57
|
+
asked
|
|
58
|
+
]);
|
|
59
|
+
// The query string is the request's identity: one search, one URL.
|
|
60
|
+
const query = orgId ? starterQueryParams({
|
|
61
|
+
orgId,
|
|
62
|
+
mode,
|
|
63
|
+
word: asked,
|
|
64
|
+
filter,
|
|
65
|
+
limit,
|
|
66
|
+
include
|
|
67
|
+
}).toString() : null;
|
|
68
|
+
const [rows, setRows] = useState([]);
|
|
69
|
+
const [labels, setLabels] = useState(NO_LABELS);
|
|
70
|
+
const [loading, setLoading] = useState(false);
|
|
71
|
+
const [settled, setSettled] = useState(false);
|
|
72
|
+
const [error, setError] = useState(null);
|
|
73
|
+
const [attempt, setAttempt] = useState(0);
|
|
74
|
+
const latest = useRef(0);
|
|
75
|
+
// A different workspace starts over: its answer is not the last one's.
|
|
76
|
+
useEffect(()=>{
|
|
77
|
+
setSettled(false);
|
|
78
|
+
setRows([]);
|
|
79
|
+
setError(null);
|
|
80
|
+
}, [
|
|
81
|
+
orgId
|
|
82
|
+
]);
|
|
83
|
+
useEffect(()=>{
|
|
84
|
+
if (!query || !uid) return undefined;
|
|
85
|
+
const ticket = ++latest.current;
|
|
86
|
+
let current = true;
|
|
87
|
+
setLoading(true);
|
|
88
|
+
void (async ()=>{
|
|
89
|
+
let status = 0;
|
|
90
|
+
try {
|
|
91
|
+
const response = await authorizedFetch(userRef.current, `${STARTER_SEARCH_PATH}?${query}`);
|
|
92
|
+
status = response.status;
|
|
93
|
+
const payload = await response.json().catch(()=>null);
|
|
94
|
+
if (!current || ticket !== latest.current) return;
|
|
95
|
+
if (response.ok) {
|
|
96
|
+
const parsed = parseStarterSearchAnswer(payload);
|
|
97
|
+
setRows(parsed.rows);
|
|
98
|
+
setLabels(parsed.labels);
|
|
99
|
+
setError(null);
|
|
100
|
+
} else {
|
|
101
|
+
setRows([]);
|
|
102
|
+
setError({
|
|
103
|
+
message: refusalMessage(status, payload),
|
|
104
|
+
status
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
} catch (unused) {
|
|
108
|
+
if (!current || ticket !== latest.current) return;
|
|
109
|
+
setRows([]);
|
|
110
|
+
setError({
|
|
111
|
+
message: 'Starters could not be loaded. Check your connection and try again.',
|
|
112
|
+
status
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
setSettled(true);
|
|
116
|
+
setLoading(false);
|
|
117
|
+
})();
|
|
118
|
+
return ()=>{
|
|
119
|
+
current = false;
|
|
120
|
+
};
|
|
121
|
+
}, [
|
|
122
|
+
query,
|
|
123
|
+
uid,
|
|
124
|
+
attempt
|
|
125
|
+
]);
|
|
126
|
+
const refresh = useCallback(()=>setAttempt((count)=>count + 1), []);
|
|
127
|
+
return {
|
|
128
|
+
rows,
|
|
129
|
+
labels,
|
|
130
|
+
loading,
|
|
131
|
+
settled,
|
|
132
|
+
error,
|
|
133
|
+
refresh
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
//# sourceMappingURL=use-starter-search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/hooks/use-starter-search.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn/app-utils/lockdown'\nimport type { SiteStarterFacets, SiteStarterMode } from '@aglyn/aglyn/app-utils/site-starter'\nimport type { SiteStarterSummary } from '@aglyn/aglyn/plugin-manager/plugin-site-starters'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport {\n parseStarterSearchAnswer,\n STARTER_SEARCH_PATH,\n starterQueryParams,\n type StarterSearchLabels,\n} from '../model/starter-query'\n\n/** How long the search waits for typing to stop before it asks, as the review queue's does. */\nexport const STARTER_SEARCH_SETTLE_MS = 300\n\n/** Why a search could not be answered: the words to show, and the status that said so (0 for none). */\nexport interface StarterSearchError {\n message: string\n status: number\n}\n\nexport interface StarterSearchInput {\n /** The workspace the site will belong to; nothing is asked while it resolves. */\n orgId: string | undefined\n mode: SiteStarterMode\n /** The person's words, as typed: asked once typing settles. */\n word: string\n filter?: Partial<SiteStarterFacets>\n limit?: number\n /** A listing the answer carries whatever the search finds: a pick being restored (AGL-4049). */\n include?: string | null\n}\n\nexport interface StarterSearchState {\n /** The starters the last answer listed, best first. */\n rows: SiteStarterSummary[]\n labels: StarterSearchLabels\n /** A request is out. */\n loading: boolean\n /** An answer has arrived for this workspace, listing starters or refusing. */\n settled: boolean\n /** Why the last search was not answered, or `null`. */\n error: StarterSearchError | null\n /** Asks the same search again. */\n refresh: () => void\n}\n\nconst NO_LABELS: StarterSearchLabels = { base: {}, fonts: {} }\n\n/** The words a refused or failed search shows. */\nfunction refusalMessage(status: number, payload: unknown): string {\n // A lockdown is not a broken search (AGL-1532): it says what is paused.\n const locked = parseLockdownRefusal(status, payload)\n if (locked) return lockdownRefusalText(locked)\n const error = (payload as { error?: unknown } | null)?.error\n return typeof error === 'string' && error\n ? error\n : `Starters could not be loaded (${status})`\n}\n\n/**\n * The starters a search finds, for \"Choose your starter\" (AGL-4049): asks\n * `GET /api/marketplace/starters` with the person's own token, once typing\n * has settled, and again whenever the workspace, the mode or a filter\n * changes. Only the newest answer is kept: a slow answer to an older search\n * never replaces a newer one.\n */\nexport function useStarterSearch({\n orgId,\n mode,\n word,\n filter,\n limit,\n include,\n}: StarterSearchInput): StarterSearchState {\n const { data: user } = useUser()\n // The token is read from the user at the moment of asking; the request is\n // keyed on WHO is asking, so a re-render handing over the same account does\n // not ask again.\n const userRef = useRef(user)\n userRef.current = user\n const uid = user?.uid ?? null\n\n const [asked, setAsked] = useState(() => word.trim())\n useEffect(() => {\n const settledWord = word.trim()\n if (settledWord === asked) return undefined\n const timer = setTimeout(() => setAsked(settledWord), STARTER_SEARCH_SETTLE_MS)\n return () => clearTimeout(timer)\n }, [word, asked])\n\n // The query string is the request's identity: one search, one URL.\n const query = orgId ? starterQueryParams({ orgId, mode, word: asked, filter, limit, include }).toString() : null\n\n const [rows, setRows] = useState<SiteStarterSummary[]>([])\n const [labels, setLabels] = useState<StarterSearchLabels>(NO_LABELS)\n const [loading, setLoading] = useState(false)\n const [settled, setSettled] = useState(false)\n const [error, setError] = useState<StarterSearchError | null>(null)\n const [attempt, setAttempt] = useState(0)\n const latest = useRef(0)\n\n // A different workspace starts over: its answer is not the last one's.\n useEffect(() => {\n setSettled(false)\n setRows([])\n setError(null)\n }, [orgId])\n\n useEffect(() => {\n if (!query || !uid) return undefined\n const ticket = ++latest.current\n let current = true\n setLoading(true)\n void (async () => {\n let status = 0\n try {\n const response = await authorizedFetch(userRef.current, `${STARTER_SEARCH_PATH}?${query}`)\n status = response.status\n const payload: unknown = await response.json().catch((): null => null)\n if (!current || ticket !== latest.current) return\n if (response.ok) {\n const parsed = parseStarterSearchAnswer(payload)\n setRows(parsed.rows)\n setLabels(parsed.labels)\n setError(null)\n } else {\n setRows([])\n setError({ message: refusalMessage(status, payload), status })\n }\n } catch {\n if (!current || ticket !== latest.current) return\n setRows([])\n setError({\n message: 'Starters could not be loaded. Check your connection and try again.',\n status,\n })\n }\n setSettled(true)\n setLoading(false)\n })()\n return () => {\n current = false\n }\n }, [query, uid, attempt])\n\n const refresh = useCallback(() => setAttempt((count) => count + 1), [])\n return { rows, labels, loading, settled, error, refresh }\n}\n"],"names":["lockdownRefusalText","parseLockdownRefusal","authorizedFetch","useUser","useCallback","useEffect","useRef","useState","parseStarterSearchAnswer","STARTER_SEARCH_PATH","starterQueryParams","STARTER_SEARCH_SETTLE_MS","NO_LABELS","base","fonts","refusalMessage","status","payload","locked","error","useStarterSearch","orgId","mode","word","filter","limit","include","data","user","userRef","current","uid","asked","setAsked","trim","settledWord","undefined","timer","setTimeout","clearTimeout","query","toString","rows","setRows","labels","setLabels","loading","setLoading","settled","setSettled","setError","attempt","setAttempt","latest","ticket","response","json","catch","ok","parsed","message","refresh","count"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAEA,SAASA,mBAAmB,EAAEC,oBAAoB,QAAQ,kCAAiC;AAG3F,SAASC,eAAe,QAAQ,2CAA0C;AAC1E,SAASC,OAAO,QAAQ,iCAAgC;AACxD,SAASC,WAAW,EAAEC,SAAS,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAO;AAChE,SACEC,wBAAwB,EACxBC,mBAAmB,EACnBC,kBAAkB,QAEb,4BAAwB;AAE/B,6FAA6F,GAC7F,OAAO,MAAMC,2BAA2B,IAAG;AAkC3C,MAAMC,YAAiC;IAAEC,MAAM,CAAC;IAAGC,OAAO,CAAC;AAAE;AAE7D,gDAAgD,GAChD,SAASC,eAAeC,MAAc,EAAEC,OAAgB;IACtD,wEAAwE;IACxE,MAAMC,SAASjB,qBAAqBe,QAAQC;IAC5C,IAAIC,QAAQ,OAAOlB,oBAAoBkB;IACvC,MAAMC,QAASF,2BAAD,AAACA,QAAwCE,KAAK;IAC5D,OAAO,OAAOA,UAAU,YAAYA,QAChCA,QACA,CAAC,8BAA8B,EAAEH,OAAO,CAAC,CAAC;AAChD;AAEA;;;;;;CAMC,GACD,OAAO,SAASI,iBAAiB,EAC/BC,KAAK,EACLC,IAAI,EACJC,IAAI,EACJC,MAAM,EACNC,KAAK,EACLC,OAAO,EACY;;IACnB,MAAM,EAAEC,MAAMC,IAAI,EAAE,GAAGzB;IACvB,0EAA0E;IAC1E,4EAA4E;IAC5E,iBAAiB;IACjB,MAAM0B,UAAUvB,OAAOsB;IACvBC,QAAQC,OAAO,GAAGF;IAClB,MAAMG,cAAMH,wBAAAA,KAAMG,GAAG,mBAAI;IAEzB,MAAM,CAACC,OAAOC,SAAS,GAAG1B,SAAS,IAAMgB,KAAKW,IAAI;IAClD7B,UAAU;QACR,MAAM8B,cAAcZ,KAAKW,IAAI;QAC7B,IAAIC,gBAAgBH,OAAO,OAAOI;QAClC,MAAMC,QAAQC,WAAW,IAAML,SAASE,cAAcxB;QACtD,OAAO,IAAM4B,aAAaF;IAC5B,GAAG;QAACd;QAAMS;KAAM;IAEhB,mEAAmE;IACnE,MAAMQ,QAAQnB,QAAQX,mBAAmB;QAAEW;QAAOC;QAAMC,MAAMS;QAAOR;QAAQC;QAAOC;IAAQ,GAAGe,QAAQ,KAAK;IAE5G,MAAM,CAACC,MAAMC,QAAQ,GAAGpC,SAA+B,EAAE;IACzD,MAAM,CAACqC,QAAQC,UAAU,GAAGtC,SAA8BK;IAC1D,MAAM,CAACkC,SAASC,WAAW,GAAGxC,SAAS;IACvC,MAAM,CAACyC,SAASC,WAAW,GAAG1C,SAAS;IACvC,MAAM,CAACY,OAAO+B,SAAS,GAAG3C,SAAoC;IAC9D,MAAM,CAAC4C,SAASC,WAAW,GAAG7C,SAAS;IACvC,MAAM8C,SAAS/C,OAAO;IAEtB,uEAAuE;IACvED,UAAU;QACR4C,WAAW;QACXN,QAAQ,EAAE;QACVO,SAAS;IACX,GAAG;QAAC7B;KAAM;IAEVhB,UAAU;QACR,IAAI,CAACmC,SAAS,CAACT,KAAK,OAAOK;QAC3B,MAAMkB,SAAS,EAAED,OAAOvB,OAAO;QAC/B,IAAIA,UAAU;QACdiB,WAAW;QACX,KAAK,AAAC,CAAA;YACJ,IAAI/B,SAAS;YACb,IAAI;gBACF,MAAMuC,WAAW,MAAMrD,gBAAgB2B,QAAQC,OAAO,EAAE,GAAGrB,oBAAoB,CAAC,EAAE+B,OAAO;gBACzFxB,SAASuC,SAASvC,MAAM;gBACxB,MAAMC,UAAmB,MAAMsC,SAASC,IAAI,GAAGC,KAAK,CAAC,IAAY;gBACjE,IAAI,CAAC3B,WAAWwB,WAAWD,OAAOvB,OAAO,EAAE;gBAC3C,IAAIyB,SAASG,EAAE,EAAE;oBACf,MAAMC,SAASnD,yBAAyBS;oBACxC0B,QAAQgB,OAAOjB,IAAI;oBACnBG,UAAUc,OAAOf,MAAM;oBACvBM,SAAS;gBACX,OAAO;oBACLP,QAAQ,EAAE;oBACVO,SAAS;wBAAEU,SAAS7C,eAAeC,QAAQC;wBAAUD;oBAAO;gBAC9D;YACF,EAAE,eAAM;gBACN,IAAI,CAACc,WAAWwB,WAAWD,OAAOvB,OAAO,EAAE;gBAC3Ca,QAAQ,EAAE;gBACVO,SAAS;oBACPU,SAAS;oBACT5C;gBACF;YACF;YACAiC,WAAW;YACXF,WAAW;QACb,CAAA;QACA,OAAO;YACLjB,UAAU;QACZ;IACF,GAAG;QAACU;QAAOT;QAAKoB;KAAQ;IAExB,MAAMU,UAAUzD,YAAY,IAAMgD,WAAW,CAACU,QAAUA,QAAQ,IAAI,EAAE;IACtE,OAAO;QAAEpB;QAAME;QAAQE;QAASE;QAAS7B;QAAO0C;IAAQ;AAC1D"}
|
|
@@ -89,8 +89,9 @@ export declare function isListingBrowsable(listing: {
|
|
|
89
89
|
workspaceLockedAt?: unknown;
|
|
90
90
|
}): boolean;
|
|
91
91
|
/**
|
|
92
|
-
* Listing fields a publisher may legitimately write
|
|
93
|
-
* (AGL-1361). Everything else must be denied in the rules'
|
|
92
|
+
* Listing fields a publisher may legitimately write from the client, each
|
|
93
|
+
* with the reason (AGL-1361). Everything else must be denied in the rules'
|
|
94
|
+
* listing key diff.
|
|
94
95
|
*
|
|
95
96
|
* They live here rather than beside `MarketplaceListing` for the same reason
|
|
96
97
|
* the visibility policy does: this module is what READS the fields that decide
|
|
@@ -98,13 +99,20 @@ export declare function isListingBrowsable(listing: {
|
|
|
98
99
|
* `aglyn:addons`. `marketplace.ts` re-exports them so publishing code keeps
|
|
99
100
|
* one import site.
|
|
100
101
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
102
|
+
* EMPTY since AGL-4105. Every publisher save agrees to the Marketplace
|
|
103
|
+
* Publisher Agreement, and only a route can refuse a save that did not and
|
|
104
|
+
* record one that did, so nothing is left for a client to write directly.
|
|
105
|
+
* The last three:
|
|
106
|
+
*
|
|
107
|
+
* - `category` and `categories`, shelf placement, written through
|
|
108
|
+
* `update-listing` like the rest of the listing (the editor always did);
|
|
109
|
+
* - `pluginId`, the manifest id a plugin listing declares (AGL-45), which
|
|
110
|
+
* only the publish route ever sets.
|
|
111
|
+
*
|
|
112
|
+
* Nothing in the repo wrote any of them client-side, so denying them cost
|
|
113
|
+
* nothing — which is how AGL-1364 closed too. `displayName`, the copy a buyer
|
|
114
|
+
* reads, and the listing artwork left this list earlier (AGL-3321, AGL-3365,
|
|
115
|
+
* AGL-1701) for their own reasons.
|
|
108
116
|
*/
|
|
109
117
|
export declare const LISTING_CLIENT_WRITABLE_FIELDS: Readonly<Record<string, string>>;
|
|
110
118
|
/**
|
|
@@ -101,8 +101,9 @@
|
|
|
101
101
|
return listing.reviewStatus === undefined || listing.reviewStatus === 'listed' || listing.reviewStatus === 'verified';
|
|
102
102
|
}
|
|
103
103
|
/**
|
|
104
|
-
* Listing fields a publisher may legitimately write
|
|
105
|
-
* (AGL-1361). Everything else must be denied in the rules'
|
|
104
|
+
* Listing fields a publisher may legitimately write from the client, each
|
|
105
|
+
* with the reason (AGL-1361). Everything else must be denied in the rules'
|
|
106
|
+
* listing key diff.
|
|
106
107
|
*
|
|
107
108
|
* They live here rather than beside `MarketplaceListing` for the same reason
|
|
108
109
|
* the visibility policy does: this module is what READS the fields that decide
|
|
@@ -110,36 +111,21 @@
|
|
|
110
111
|
* `aglyn:addons`. `marketplace.ts` re-exports them so publishing code keeps
|
|
111
112
|
* one import site.
|
|
112
113
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// before storing it, and a client write would publish it unscreened, so
|
|
129
|
-
// they are written through `update-listing` alone and denied here.
|
|
130
|
-
category: 'The single legacy category. Publisher-chosen shelf placement; it ' + 'orders a browse page and gates nothing.',
|
|
131
|
-
categories: 'The AGL-430 multi-category list. Same reasoning as `category` — ' + 'placement, not permission.',
|
|
132
|
-
// `logoUrl` and `screenshots` were here, justified as "sanitized at
|
|
133
|
-
// render" (AGL-1701). They were not: listing-content.component.tsx hands
|
|
134
|
-
// both straight to an `<img src>`, and `logoUrl` additionally becomes an
|
|
135
|
-
// `og:image` served to unauthenticated unfurlers. Publisher-authored
|
|
136
|
-
// ARTWORK is not like publisher-authored COPY — copy is read by the
|
|
137
|
-
// viewer, artwork is FETCHED by the viewer's browser from whatever host
|
|
138
|
-
// the string names, disclosing their IP and a `Referer` naming the
|
|
139
|
-
// console and their org to a recipient on no subprocessor register.
|
|
140
|
-
// Denied in the rules and held to first-party media on the API path.
|
|
141
|
-
pluginId: 'The manifest id a plugin listing declares (AGL-45). Rewriting it ' + 'breaks the publisher\'s own install resolution; the artifact that ' + 'actually runs is pinned by sha256 in the staff-only `pluginVersions`.'
|
|
142
|
-
};
|
|
114
|
+
* EMPTY since AGL-4105. Every publisher save agrees to the Marketplace
|
|
115
|
+
* Publisher Agreement, and only a route can refuse a save that did not and
|
|
116
|
+
* record one that did, so nothing is left for a client to write directly.
|
|
117
|
+
* The last three:
|
|
118
|
+
*
|
|
119
|
+
* - `category` and `categories`, shelf placement, written through
|
|
120
|
+
* `update-listing` like the rest of the listing (the editor always did);
|
|
121
|
+
* - `pluginId`, the manifest id a plugin listing declares (AGL-45), which
|
|
122
|
+
* only the publish route ever sets.
|
|
123
|
+
*
|
|
124
|
+
* Nothing in the repo wrote any of them client-side, so denying them cost
|
|
125
|
+
* nothing — which is how AGL-1364 closed too. `displayName`, the copy a buyer
|
|
126
|
+
* reads, and the listing artwork left this list earlier (AGL-3321, AGL-3365,
|
|
127
|
+
* AGL-1701) for their own reasons.
|
|
128
|
+
*/ export const LISTING_CLIENT_WRITABLE_FIELDS = {};
|
|
143
129
|
/**
|
|
144
130
|
* Listing keys that never reach Firestore, so the rules have no opinion.
|
|
145
131
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/model/listing-visibility.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Who is allowed to see a marketplace listing (AGL-432/658/968/1196).\n *\n * One reading of \"is this listing public\", on purpose. The rule gates browse\n * AND install (AGL-965 added the second call site after the first proved\n * insufficient on its own), and the listing's social card (AGL-876) is\n * emitted to an UNAUTHENTICATED fetcher, so it applies the same rule the\n * browse grid applies — or a rejected, private or taken-down listing gets its\n * name and description unfurled into a chat message by anyone holding the\n * URL. A third reading that drifts would be a disclosure bug, not a cosmetic\n * one.\n *\n * These lived in core while a console route had to ask the question and a\n * `scope:app` project may not depend on an `aglyn:addons` lib. The listing\n * route and its card are the plugin's own now (AGL-3080), so the policy is\n * too. `model/marketplace.ts` re-exports all four, so publishing and install\n * code keeps importing them from one place.\n */\n\nimport type { InstallableArtifactType as MarketplaceArtifactType } from '@aglyn/aglyn/app-utils/artifact-provenance'\n\n/**\n * The listing's artifact type, tolerating the pre-AGL-654 shape.\n *\n * Legacy listings carry `type`/`kind` instead; a component was the absence\n * of both, which is why this defaults there rather than throwing. Keeping\n * the fallback means old docs keep resolving correctly instead of silently\n * becoming un-installable.\n */\nexport function listingArtifactType(listing: {\n artifactType?: string\n type?: string\n kind?: string\n}): MarketplaceArtifactType {\n if (listing.artifactType) return listing.artifactType as MarketplaceArtifactType\n if (listing.kind === 'template') return 'template'\n if (listing.type === 'plugin') return 'plugin'\n return 'component'\n}\n\n/**\n * Private plugins (AGL-968): an org builds for its own sites without\n * publishing to anyone. Never browsable — not even for the owner, who\n * reaches it through their own plugins area — and installable only by the\n * owning org (enforced in `install-plugin`, not in the UI). The review bar\n * is identical: private code still runs on our infrastructure.\n */\nexport function isPrivateListing(listing: { visibility?: string }): boolean {\n return listing.visibility === 'private'\n}\n\n/**\n * Whether a listing has been soft-deleted (AGL-1196).\n *\n * Browse used to express this as `where('deletedAt','==',null)` in the query\n * itself. That put a MUTABLE field in a predicate: `deletedAt` flips on every\n * unpublish/republish, and a document that stops matching a live query can\n * leave a `noDocument` tombstone at its own path — which the detail page then\n * reads BY ID and 404s on (AGL-827, AGL-929). Filtering in memory removes the\n * mechanism instead of healing it.\n *\n * FALSY, not `=== null`. Firestore's `== null` matches only an explicit null\n * and cannot express \"field is absent\", so a listing written without the field\n * was invisible to the old query. Absent means live.\n *\n * Deliberately NOT folded into `isListingBrowsable`: that check carries an\n * owner exemption, so a publisher can watch their own submission move through\n * review. Deletion has no such exemption — a deleted listing is gone for\n * everyone, including the org that owns it.\n */\nexport function isListingDeleted(listing: { deletedAt?: unknown }): boolean {\n return Boolean(listing.deletedAt)\n}\n\n/**\n * The artifact types that wait for review before they are browsable: plugins,\n * which execute code (AGL-432), and site starters, which create a whole site\n * the moment they are picked, with no step between to catch what a publisher\n * got wrong (AGL-4049).\n */\nconst REVIEWED_ARTIFACT_TYPES: ReadonlySet<MarketplaceArtifactType> = new Set(['plugin', 'siteStarter'])\n\n/** Whether a plugin listing is publicly browsable (AGL-432). */\nexport function isListingBrowsable(listing: {\n artifactType?: string\n type?: string\n kind?: string\n reviewStatus?: string\n hiddenAt?: unknown\n visibility?: string\n workspaceLockedAt?: unknown\n}): boolean {\n // Staff takedown applies to EVERY artifact type (AGL-658). Pre-publication\n // review is for plugins and site starters only (REVIEWED_ARTIFACT_TYPES),\n // which earn the wait — but a component or template that turns out to be\n // abusive must be removable too, and before this it simply was not: the\n // early return below meant anything non-plugin was permanently browsable.\n if (listing.hiddenAt) return false\n // The publishing workspace is locked (AGL-3365), for any reason: its\n // listings leave browse, search and their pages until the lift. Its own\n // field rather than `hiddenAt`, which is staff's takedown and also stops\n // already-installed plugins from loading — a lock over an unpaid bill must\n // not break a buyer's site, and the lift must not undo a takedown.\n if (listing.workspaceLockedAt) return false\n // Private listings never reach the marketplace (AGL-968).\n if (isPrivateListing(listing)) return false\n if (!REVIEWED_ARTIFACT_TYPES.has(listingArtifactType(listing))) return true\n return (\n listing.reviewStatus === undefined ||\n listing.reviewStatus === 'listed' ||\n listing.reviewStatus === 'verified'\n )\n}\n\n/**\n * Listing fields a publisher may legitimately write, each with the reason\n * (AGL-1361). Everything else must be denied in the rules' listing key diff.\n *\n * They live here rather than beside `MarketplaceListing` for the same reason\n * the visibility policy does: this module is what READS the fields that decide\n * whether a listing is browsable, and `scope:app` may not depend on\n * `aglyn:addons`. `marketplace.ts` re-exports them so publishing code keeps\n * one import site.\n *\n * The list is short on purpose. Nothing in the repo writes a listing\n * document client-side any more: the unpublish/republish toggle in the org\n * seller panel was the last, and it goes through the publish API since\n * AGL-3321 — as publish, content edits, review and install always did. So\n * the fields below are classified writable because a publisher OWNS them,\n * not because anything would break: denying the rest cost nothing, which is\n * how AGL-1364 closed.\n */\nexport const LISTING_CLIENT_WRITABLE_FIELDS: Readonly<Record<string, string>> =\n {\n // `displayName` was here until AGL-3321: the marketplace lists search it\n // through keys the server derives, so it is written through the API\n // (`update-listing`) alone and denied here with `deletedAt` and\n // `visibility`, the two other fields a listing's browse audience follows.\n // `description`, `readme`, `homepageUrl`, `repositoryUrl` and `license`\n // were here as \"publisher-authored, sanitized at render\" (AGL-430). Since\n // AGL-3365 the API runs what a buyer reads through the phishing screen\n // before storing it, and a client write would publish it unscreened, so\n // they are written through `update-listing` alone and denied here.\n category:\n 'The single legacy category. Publisher-chosen shelf placement; it ' +\n 'orders a browse page and gates nothing.',\n categories:\n 'The AGL-430 multi-category list. Same reasoning as `category` — ' +\n 'placement, not permission.',\n // `logoUrl` and `screenshots` were here, justified as \"sanitized at\n // render\" (AGL-1701). They were not: listing-content.component.tsx hands\n // both straight to an `<img src>`, and `logoUrl` additionally becomes an\n // `og:image` served to unauthenticated unfurlers. Publisher-authored\n // ARTWORK is not like publisher-authored COPY — copy is read by the\n // viewer, artwork is FETCHED by the viewer's browser from whatever host\n // the string names, disclosing their IP and a `Referer` naming the\n // console and their org to a recipient on no subprocessor register.\n // Denied in the rules and held to first-party media on the API path.\n pluginId:\n 'The manifest id a plugin listing declares (AGL-45). Rewriting it ' +\n 'breaks the publisher\\'s own install resolution; the artifact that ' +\n 'actually runs is pinned by sha256 in the staff-only `pluginVersions`.',\n }\n\n/**\n * Listing keys that never reach Firestore, so the rules have no opinion.\n *\n * Empty today — `MarketplaceListing` declares no `$id`, because listing code\n * carries the id alongside the data rather than folding it in. Kept as the\n * partition's third arm so a future projection field has a home that is not\n * \"client-writable\".\n */\nexport const LISTING_UNPERSISTED_FIELDS: Readonly<Record<string, string>> = {}\n"],"names":["listingArtifactType","listing","artifactType","kind","type","isPrivateListing","visibility","isListingDeleted","Boolean","deletedAt","REVIEWED_ARTIFACT_TYPES","Set","isListingBrowsable","hiddenAt","workspaceLockedAt","has","reviewStatus","undefined","LISTING_CLIENT_WRITABLE_FIELDS","category","categories","pluginId","LISTING_UNPERSISTED_FIELDS"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;CAiBC,GAID;;;;;;;CAOC,GACD,OAAO,SAASA,oBAAoBC,OAInC;IACC,IAAIA,QAAQC,YAAY,EAAE,OAAOD,QAAQC,YAAY;IACrD,IAAID,QAAQE,IAAI,KAAK,YAAY,OAAO;IACxC,IAAIF,QAAQG,IAAI,KAAK,UAAU,OAAO;IACtC,OAAO;AACT;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,iBAAiBJ,OAAgC;IAC/D,OAAOA,QAAQK,UAAU,KAAK;AAChC;AAEA;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC,iBAAiBN,OAAgC;IAC/D,OAAOO,QAAQP,QAAQQ,SAAS;AAClC;AAEA;;;;;CAKC,GACD,MAAMC,0BAAgE,IAAIC,IAAI;IAAC;IAAU;CAAc;AAEvG,8DAA8D,GAC9D,OAAO,SAASC,mBAAmBX,OAQlC;IACC,2EAA2E;IAC3E,0EAA0E;IAC1E,yEAAyE;IACzE,wEAAwE;IACxE,0EAA0E;IAC1E,IAAIA,QAAQY,QAAQ,EAAE,OAAO;IAC7B,qEAAqE;IACrE,wEAAwE;IACxE,yEAAyE;IACzE,2EAA2E;IAC3E,mEAAmE;IACnE,IAAIZ,QAAQa,iBAAiB,EAAE,OAAO;IACtC,0DAA0D;IAC1D,IAAIT,iBAAiBJ,UAAU,OAAO;IACtC,IAAI,CAACS,wBAAwBK,GAAG,CAACf,oBAAoBC,WAAW,OAAO;IACvE,OACEA,QAAQe,YAAY,KAAKC,aACzBhB,QAAQe,YAAY,KAAK,YACzBf,QAAQe,YAAY,KAAK;AAE7B;AAEA;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,MAAME,iCACX;IACE,yEAAyE;IACzE,oEAAoE;IACpE,gEAAgE;IAChE,0EAA0E;IAC1E,wEAAwE;IACxE,0EAA0E;IAC1E,uEAAuE;IACvE,wEAAwE;IACxE,mEAAmE;IACnEC,UACE,sEACA;IACFC,YACE,qEACA;IACF,oEAAoE;IACpE,yEAAyE;IACzE,yEAAyE;IACzE,qEAAqE;IACrE,oEAAoE;IACpE,wEAAwE;IACxE,mEAAmE;IACnE,oEAAoE;IACpE,qEAAqE;IACrEC,UACE,sEACA,uEACA;AACJ,EAAC;AAEH;;;;;;;CAOC,GACD,OAAO,MAAMC,6BAA+D,CAAC,EAAC"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/model/listing-visibility.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Who is allowed to see a marketplace listing (AGL-432/658/968/1196).\n *\n * One reading of \"is this listing public\", on purpose. The rule gates browse\n * AND install (AGL-965 added the second call site after the first proved\n * insufficient on its own), and the listing's social card (AGL-876) is\n * emitted to an UNAUTHENTICATED fetcher, so it applies the same rule the\n * browse grid applies — or a rejected, private or taken-down listing gets its\n * name and description unfurled into a chat message by anyone holding the\n * URL. A third reading that drifts would be a disclosure bug, not a cosmetic\n * one.\n *\n * These lived in core while a console route had to ask the question and a\n * `scope:app` project may not depend on an `aglyn:addons` lib. The listing\n * route and its card are the plugin's own now (AGL-3080), so the policy is\n * too. `model/marketplace.ts` re-exports all four, so publishing and install\n * code keeps importing them from one place.\n */\n\nimport type { InstallableArtifactType as MarketplaceArtifactType } from '@aglyn/aglyn/app-utils/artifact-provenance'\n\n/**\n * The listing's artifact type, tolerating the pre-AGL-654 shape.\n *\n * Legacy listings carry `type`/`kind` instead; a component was the absence\n * of both, which is why this defaults there rather than throwing. Keeping\n * the fallback means old docs keep resolving correctly instead of silently\n * becoming un-installable.\n */\nexport function listingArtifactType(listing: {\n artifactType?: string\n type?: string\n kind?: string\n}): MarketplaceArtifactType {\n if (listing.artifactType) return listing.artifactType as MarketplaceArtifactType\n if (listing.kind === 'template') return 'template'\n if (listing.type === 'plugin') return 'plugin'\n return 'component'\n}\n\n/**\n * Private plugins (AGL-968): an org builds for its own sites without\n * publishing to anyone. Never browsable — not even for the owner, who\n * reaches it through their own plugins area — and installable only by the\n * owning org (enforced in `install-plugin`, not in the UI). The review bar\n * is identical: private code still runs on our infrastructure.\n */\nexport function isPrivateListing(listing: { visibility?: string }): boolean {\n return listing.visibility === 'private'\n}\n\n/**\n * Whether a listing has been soft-deleted (AGL-1196).\n *\n * Browse used to express this as `where('deletedAt','==',null)` in the query\n * itself. That put a MUTABLE field in a predicate: `deletedAt` flips on every\n * unpublish/republish, and a document that stops matching a live query can\n * leave a `noDocument` tombstone at its own path — which the detail page then\n * reads BY ID and 404s on (AGL-827, AGL-929). Filtering in memory removes the\n * mechanism instead of healing it.\n *\n * FALSY, not `=== null`. Firestore's `== null` matches only an explicit null\n * and cannot express \"field is absent\", so a listing written without the field\n * was invisible to the old query. Absent means live.\n *\n * Deliberately NOT folded into `isListingBrowsable`: that check carries an\n * owner exemption, so a publisher can watch their own submission move through\n * review. Deletion has no such exemption — a deleted listing is gone for\n * everyone, including the org that owns it.\n */\nexport function isListingDeleted(listing: { deletedAt?: unknown }): boolean {\n return Boolean(listing.deletedAt)\n}\n\n/**\n * The artifact types that wait for review before they are browsable: plugins,\n * which execute code (AGL-432), and site starters, which create a whole site\n * the moment they are picked, with no step between to catch what a publisher\n * got wrong (AGL-4049).\n */\nconst REVIEWED_ARTIFACT_TYPES: ReadonlySet<MarketplaceArtifactType> = new Set(['plugin', 'siteStarter'])\n\n/** Whether a plugin listing is publicly browsable (AGL-432). */\nexport function isListingBrowsable(listing: {\n artifactType?: string\n type?: string\n kind?: string\n reviewStatus?: string\n hiddenAt?: unknown\n visibility?: string\n workspaceLockedAt?: unknown\n}): boolean {\n // Staff takedown applies to EVERY artifact type (AGL-658). Pre-publication\n // review is for plugins and site starters only (REVIEWED_ARTIFACT_TYPES),\n // which earn the wait — but a component or template that turns out to be\n // abusive must be removable too, and before this it simply was not: the\n // early return below meant anything non-plugin was permanently browsable.\n if (listing.hiddenAt) return false\n // The publishing workspace is locked (AGL-3365), for any reason: its\n // listings leave browse, search and their pages until the lift. Its own\n // field rather than `hiddenAt`, which is staff's takedown and also stops\n // already-installed plugins from loading — a lock over an unpaid bill must\n // not break a buyer's site, and the lift must not undo a takedown.\n if (listing.workspaceLockedAt) return false\n // Private listings never reach the marketplace (AGL-968).\n if (isPrivateListing(listing)) return false\n if (!REVIEWED_ARTIFACT_TYPES.has(listingArtifactType(listing))) return true\n return (\n listing.reviewStatus === undefined ||\n listing.reviewStatus === 'listed' ||\n listing.reviewStatus === 'verified'\n )\n}\n\n/**\n * Listing fields a publisher may legitimately write from the client, each\n * with the reason (AGL-1361). Everything else must be denied in the rules'\n * listing key diff.\n *\n * They live here rather than beside `MarketplaceListing` for the same reason\n * the visibility policy does: this module is what READS the fields that decide\n * whether a listing is browsable, and `scope:app` may not depend on\n * `aglyn:addons`. `marketplace.ts` re-exports them so publishing code keeps\n * one import site.\n *\n * EMPTY since AGL-4105. Every publisher save agrees to the Marketplace\n * Publisher Agreement, and only a route can refuse a save that did not and\n * record one that did, so nothing is left for a client to write directly.\n * The last three:\n *\n * - `category` and `categories`, shelf placement, written through\n * `update-listing` like the rest of the listing (the editor always did);\n * - `pluginId`, the manifest id a plugin listing declares (AGL-45), which\n * only the publish route ever sets.\n *\n * Nothing in the repo wrote any of them client-side, so denying them cost\n * nothing — which is how AGL-1364 closed too. `displayName`, the copy a buyer\n * reads, and the listing artwork left this list earlier (AGL-3321, AGL-3365,\n * AGL-1701) for their own reasons.\n */\nexport const LISTING_CLIENT_WRITABLE_FIELDS: Readonly<Record<string, string>> =\n {}\n\n/**\n * Listing keys that never reach Firestore, so the rules have no opinion.\n *\n * Empty today — `MarketplaceListing` declares no `$id`, because listing code\n * carries the id alongside the data rather than folding it in. Kept as the\n * partition's third arm so a future projection field has a home that is not\n * \"client-writable\".\n */\nexport const LISTING_UNPERSISTED_FIELDS: Readonly<Record<string, string>> = {}\n"],"names":["listingArtifactType","listing","artifactType","kind","type","isPrivateListing","visibility","isListingDeleted","Boolean","deletedAt","REVIEWED_ARTIFACT_TYPES","Set","isListingBrowsable","hiddenAt","workspaceLockedAt","has","reviewStatus","undefined","LISTING_CLIENT_WRITABLE_FIELDS","LISTING_UNPERSISTED_FIELDS"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;CAiBC,GAID;;;;;;;CAOC,GACD,OAAO,SAASA,oBAAoBC,OAInC;IACC,IAAIA,QAAQC,YAAY,EAAE,OAAOD,QAAQC,YAAY;IACrD,IAAID,QAAQE,IAAI,KAAK,YAAY,OAAO;IACxC,IAAIF,QAAQG,IAAI,KAAK,UAAU,OAAO;IACtC,OAAO;AACT;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,iBAAiBJ,OAAgC;IAC/D,OAAOA,QAAQK,UAAU,KAAK;AAChC;AAEA;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC,iBAAiBN,OAAgC;IAC/D,OAAOO,QAAQP,QAAQQ,SAAS;AAClC;AAEA;;;;;CAKC,GACD,MAAMC,0BAAgE,IAAIC,IAAI;IAAC;IAAU;CAAc;AAEvG,8DAA8D,GAC9D,OAAO,SAASC,mBAAmBX,OAQlC;IACC,2EAA2E;IAC3E,0EAA0E;IAC1E,yEAAyE;IACzE,wEAAwE;IACxE,0EAA0E;IAC1E,IAAIA,QAAQY,QAAQ,EAAE,OAAO;IAC7B,qEAAqE;IACrE,wEAAwE;IACxE,yEAAyE;IACzE,2EAA2E;IAC3E,mEAAmE;IACnE,IAAIZ,QAAQa,iBAAiB,EAAE,OAAO;IACtC,0DAA0D;IAC1D,IAAIT,iBAAiBJ,UAAU,OAAO;IACtC,IAAI,CAACS,wBAAwBK,GAAG,CAACf,oBAAoBC,WAAW,OAAO;IACvE,OACEA,QAAQe,YAAY,KAAKC,aACzBhB,QAAQe,YAAY,KAAK,YACzBf,QAAQe,YAAY,KAAK;AAE7B;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;CAyBC,GACD,OAAO,MAAME,iCACX,CAAC,EAAC;AAEJ;;;;;;;CAOC,GACD,OAAO,MAAMC,6BAA+D,CAAC,EAAC"}
|