@stage5/lumine 0.3.18 → 0.3.19
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/README.md +5 -2
- package/lib/admin-receipts.js +27 -0
- package/lib/admin.js +47 -3
- package/lib/api.js +18 -0
- package/lib/commands.js +53 -2
- package/lib/constants.js +3 -2
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +2 -1
package/README.md
CHANGED
|
@@ -96,8 +96,11 @@ run `lumine suggestions <build-url-or-id>` to see their open suggestion inbox.
|
|
|
96
96
|
The inbox prints canonical follow-up commands for merging or replacing Main and
|
|
97
97
|
for applying the exact frozen thumbnail shown in a suggestion. The same actions
|
|
98
98
|
are also available directly as `lumine suggestions merge <id>`,
|
|
99
|
-
`lumine suggestions replace-main <id>`,
|
|
100
|
-
`lumine suggestions adopt-thumbnail <id
|
|
99
|
+
`lumine suggestions replace-main <id>`,
|
|
100
|
+
`lumine suggestions adopt-thumbnail <id>`, and
|
|
101
|
+
`lumine suggestions adopt-title <id>`. To pass on a branch, thumbnail or name
|
|
102
|
+
suggestion, run `lumine suggestions decline <id>`; the chat card shows it was
|
|
103
|
+
declined for both sides and it leaves the inbox. Large inboxes are cursor-paginated;
|
|
101
104
|
the CLI prints the exact `--cursor` command for the next page.
|
|
102
105
|
|
|
103
106
|
Use `lumine explore` to list public open-source Build apps that can be used as
|
package/lib/admin-receipts.js
CHANGED
|
@@ -40,6 +40,33 @@ export function receiptToKeep(operation, result) {
|
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
default:
|
|
43
|
+
// Bridge Builder writes answer with the whole crew view: members' who-
|
|
44
|
+
// are-you answers, parents' addresses and questions, held parent emails.
|
|
45
|
+
// Keep the decision only (ids, statuses, counts), never the crew.
|
|
46
|
+
if (String(operation?.name || "").startsWith("meetup.")) {
|
|
47
|
+
const crew = data.crew || {};
|
|
48
|
+
return keep({
|
|
49
|
+
crewId: crew.crewId ?? data.result?.crewId ?? null,
|
|
50
|
+
crewStatus: crew.status ?? null,
|
|
51
|
+
currentStep: crew.progress?.currentStep ?? null,
|
|
52
|
+
result: scalarsOf(data.result),
|
|
53
|
+
...(data.slot ? { slot: scalarsOf(data.slot) } : {}),
|
|
54
|
+
...(data.unlocks ? { unlocks: scalarsOf(data.unlocks) } : {}),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
43
57
|
return result;
|
|
44
58
|
}
|
|
45
59
|
}
|
|
60
|
+
|
|
61
|
+
// Numbers, booleans, short status words and arrays of ids: what proves a
|
|
62
|
+
// decision without copying anyone's text.
|
|
63
|
+
function scalarsOf(value) {
|
|
64
|
+
if (!value || typeof value !== "object") return null;
|
|
65
|
+
const out = {};
|
|
66
|
+
for (const [key, item] of Object.entries(value)) {
|
|
67
|
+
if (typeof item === "number" || typeof item === "boolean" || item === null) out[key] = item;
|
|
68
|
+
else if (typeof item === "string" && /^[\w:.-]{0,40}$/.test(item)) out[key] = item;
|
|
69
|
+
else if (Array.isArray(item) && item.every((x) => typeof x === "number")) out[key] = item;
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
package/lib/admin.js
CHANGED
|
@@ -2583,7 +2583,7 @@ export function parseAdminOperation(options) {
|
|
|
2583
2583
|
// reviews as the admin controls on the website quest page. Mikey's own
|
|
2584
2584
|
// decision, any time, never in a daily run.
|
|
2585
2585
|
const usage =
|
|
2586
|
-
"Usage: lumine admin meetup list [--status review|active|completed|all] | approve-crew <crewId> | approve-grownup <crewId> | emails [list|send <id>|discard <id>|review on|off] | friends [status|on|off] | parent-reply <crewId> <memberUserId> --note <text> | funnel [--days <1-90>] | branches [list|show|approve|reject|merge] | show <crewId> | approve-plan <crewId> [--note <text>] | send-back <crewId> --note <text> | approve <crewId> --attended <userId,userId,...> [--note <text>] | slot <crewId> --confirm <slotIndex> | resend-email <crewId>.";
|
|
2586
|
+
"Usage: lumine admin meetup list [--status review|active|completed|all] | approve-crew <crewId> | approve-grownup <crewId> | emails [list|send <id>|discard <id>|review on|off] | friends [status|on|off] | parent-reply <crewId> <memberUserId> --note <text> | funnel [--days <1-90>] | branches [list|show|approve|reject|merge] | show <crewId> | approve-plan <crewId> [--note <text>] | send-back <crewId> --note <text> | approve <crewId> --attended <userId,userId,...> [--note <text>] | info <crewId> <userId> --decision ask|accept|ask-again|withdraw [--note <text>] | slot <crewId> --confirm <slotIndex> | resend-email <crewId>.";
|
|
2587
2587
|
const note = String(options.note || "").trim();
|
|
2588
2588
|
if (note.length > 1000) {
|
|
2589
2589
|
throw cliValidationError("--note must be at most 1000 characters.");
|
|
@@ -2748,6 +2748,29 @@ export function parseAdminOperation(options) {
|
|
|
2748
2748
|
{ requiresRun: false },
|
|
2749
2749
|
);
|
|
2750
2750
|
}
|
|
2751
|
+
if (action === "info") {
|
|
2752
|
+
// Staff's "who are you" check on a member (ruled 2026-10-07): the member
|
|
2753
|
+
// gives their teacher's name and class name (or how they know the crew)
|
|
2754
|
+
// on the crew page; the crew waits until you accept. Nobody is contacted.
|
|
2755
|
+
const crewId = parseRequiredInteger(target, "Crew ID", 1);
|
|
2756
|
+
const memberId = parseRequiredInteger(extra, "Member user ID", 1);
|
|
2757
|
+
const decision = parseChoice(options.adminDecision || "", "--decision", [
|
|
2758
|
+
"ask",
|
|
2759
|
+
"accept",
|
|
2760
|
+
"ask-again",
|
|
2761
|
+
"withdraw",
|
|
2762
|
+
]);
|
|
2763
|
+
if (decision === "ask-again" && !note) {
|
|
2764
|
+
throw cliValidationError("--decision ask-again needs --note <what to tell the member>.");
|
|
2765
|
+
}
|
|
2766
|
+
return writeOperation(
|
|
2767
|
+
"meetup.info",
|
|
2768
|
+
"POST",
|
|
2769
|
+
`/cli/admin/meetup-quest/crews/${crewId}/members/${memberId}/info-check`,
|
|
2770
|
+
{ action: decision === "ask" ? "request" : decision, note },
|
|
2771
|
+
{ requiresRun: false },
|
|
2772
|
+
);
|
|
2773
|
+
}
|
|
2751
2774
|
if (action === "approve-crew" || action === "approve-grownup") {
|
|
2752
2775
|
// Every step passes through the owner: the crew (members + branches) and
|
|
2753
2776
|
// the grown-up step (parents' yes + the named adult).
|
|
@@ -5716,7 +5739,7 @@ function formatMeetupMembers(members) {
|
|
|
5716
5739
|
return (members || [])
|
|
5717
5740
|
.map(
|
|
5718
5741
|
(member) =>
|
|
5719
|
-
`${member.username || `user ${member.userId}`} (${member.userId}, ${member.branch || "no branch"}${member.parentOk === undefined ? "" : member.parentOk ? ", parent OK" : ", parent not yet"}${member.attended ? ", attended" : ""})`,
|
|
5742
|
+
`${member.username || `user ${member.userId}`} (${member.userId}, ${member.branch || "no branch"}${member.parentOk === undefined ? "" : member.parentOk ? ", parent OK" : ", parent not yet"}${member.attended ? ", attended" : ""}${member.infoCheck === "requested" ? ", details asked" : member.infoCheck === "answered" ? ", ANSWERED: review" : ""})`,
|
|
5720
5743
|
)
|
|
5721
5744
|
.join("; ");
|
|
5722
5745
|
}
|
|
@@ -5731,6 +5754,17 @@ function printMeetupCrew(crew) {
|
|
|
5731
5754
|
console.log(` ${step.number}. ${step.label} [${step.state}] ${step.detail}`);
|
|
5732
5755
|
}
|
|
5733
5756
|
console.log(` Members: ${formatMeetupMembers(crew.members)}`);
|
|
5757
|
+
// staff's "who are you" checks on these members (any crew), newest first
|
|
5758
|
+
for (const check of crew.memberChecks || []) {
|
|
5759
|
+
const answer = check.teacherName
|
|
5760
|
+
? `teacher "${check.teacherName}", class "${check.className}"`
|
|
5761
|
+
: check.relationship
|
|
5762
|
+
? `how they know the crew: "${check.relationship}"`
|
|
5763
|
+
: "no answer yet";
|
|
5764
|
+
console.log(
|
|
5765
|
+
` Who-are-you check #${check.checkId} · ${check.username} (${check.userId}) · crew #${check.crewId} · ${check.status}${check.askNote ? ` · asked: "${check.askNote}"` : ""} · ${answer}${check.reviewedBy ? ` · ${check.status} by ${check.reviewedBy}` : ""}${check.staffNote ? ` · note: ${check.staffNote}` : ""}`,
|
|
5766
|
+
);
|
|
5767
|
+
}
|
|
5734
5768
|
for (const parent of crew.parentContacts || []) {
|
|
5735
5769
|
console.log(
|
|
5736
5770
|
` Parent of ${parent.childUsername}: ${parent.status}${parent.verifiedGuardian ? " (guardian on file)" : parent.staffVerified ? " (staff verified)" : " (child-typed address: NOT verified)"} · ${parent.email}${parent.question ? ` · asks: "${parent.question}"` : ""}`,
|
|
@@ -5875,11 +5909,21 @@ function printMeetupResult({ operation, data, result }) {
|
|
|
5875
5909
|
}
|
|
5876
5910
|
if (crews.some((crew) => crew.waitingFor)) {
|
|
5877
5911
|
console.log(
|
|
5878
|
-
"Review: lumine admin meetup show <crewId>, then approve-crew | approve-plan | approve-grownup | send-back --note | approve --attended <ids> (classroom plans: approve at the headmaster desk with a room and times).",
|
|
5912
|
+
"Review: lumine admin meetup show <crewId>, then approve-crew | approve-plan | approve-grownup | send-back --note | approve --attended <ids> | info <crewId> <userId> --decision accept|ask-again (a member's who-are-you answer) (classroom plans: approve at the headmaster desk with a room and times).",
|
|
5879
5913
|
);
|
|
5880
5914
|
}
|
|
5881
5915
|
return;
|
|
5882
5916
|
}
|
|
5917
|
+
if (operation.name === "meetup.info") {
|
|
5918
|
+
const status = data.result?.status;
|
|
5919
|
+
console.log(
|
|
5920
|
+
result?.changed === false
|
|
5921
|
+
? `Member ${data.result?.userId} already has an open question (${status}).`
|
|
5922
|
+
: `Member ${data.result?.userId} on crew #${data.result?.crewId}: ${status === "requested" ? "asked (their crew page now asks for their teacher and class, or how they know the crew)" : status}.`,
|
|
5923
|
+
);
|
|
5924
|
+
if (data.crew) printMeetupCrew(data.crew);
|
|
5925
|
+
return;
|
|
5926
|
+
}
|
|
5883
5927
|
if (operation.name === "meetup.show" || operation.name === "meetup.parent-reply") {
|
|
5884
5928
|
printMeetupCrew(data.crew || {});
|
|
5885
5929
|
return;
|
package/lib/api.js
CHANGED
|
@@ -530,6 +530,24 @@ export async function adoptBuildTitleSuggestion({
|
|
|
530
530
|
});
|
|
531
531
|
}
|
|
532
532
|
|
|
533
|
+
// The owner passing on one teammate suggestion (branch updates, thumbnail or
|
|
534
|
+
// name): the card settles as declined for both sides and leaves the inbox.
|
|
535
|
+
export async function declineBuildOwnerSuggestion({
|
|
536
|
+
options,
|
|
537
|
+
auth,
|
|
538
|
+
rootBuildId,
|
|
539
|
+
contributionBuildId,
|
|
540
|
+
suggestionMessageId,
|
|
541
|
+
}) {
|
|
542
|
+
return await requestJson({
|
|
543
|
+
method: "POST",
|
|
544
|
+
url: `${options.apiUrl}/build/${rootBuildId}/contributions/${contributionBuildId}/decline-suggestion`,
|
|
545
|
+
authToken: auth.token,
|
|
546
|
+
body: { suggestionMessageId },
|
|
547
|
+
timeoutMs: options.timeoutMs,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
533
551
|
export async function mergeContributionIntoMain({
|
|
534
552
|
options,
|
|
535
553
|
auth,
|
package/lib/commands.js
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import {
|
|
24
24
|
adoptBuildThumbnailSuggestion,
|
|
25
25
|
adoptBuildTitleSuggestion,
|
|
26
|
+
declineBuildOwnerSuggestion,
|
|
26
27
|
createBuild,
|
|
27
28
|
fetchAllRuntimeAssets,
|
|
28
29
|
forkBuild,
|
|
@@ -764,11 +765,50 @@ export async function suggestions(options) {
|
|
|
764
765
|
return;
|
|
765
766
|
}
|
|
766
767
|
|
|
768
|
+
if (action === "decline") {
|
|
769
|
+
const description = describeSuggestionForDecline(suggestion);
|
|
770
|
+
if (!options.assumeYes) {
|
|
771
|
+
const confirmed = await confirmPrompt(
|
|
772
|
+
`Decline suggestion #${suggestionId} (${description}) on Build #${rootBuildId}? [y/N] `,
|
|
773
|
+
);
|
|
774
|
+
if (confirmed === null) {
|
|
775
|
+
console.log("Not a TTY — re-run with --yes to decline the suggestion.");
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
if (!confirmed) {
|
|
779
|
+
console.log("Aborted. Suggestion left open.");
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
await declineBuildOwnerSuggestion({
|
|
784
|
+
options,
|
|
785
|
+
auth,
|
|
786
|
+
rootBuildId,
|
|
787
|
+
contributionBuildId,
|
|
788
|
+
suggestionMessageId: suggestionId,
|
|
789
|
+
});
|
|
790
|
+
console.log(
|
|
791
|
+
`Declined suggestion #${suggestionId} (${description}) on Build #${rootBuildId}.`,
|
|
792
|
+
);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
|
|
767
796
|
throw new Error(
|
|
768
|
-
"Usage: lumine suggestions [build] | lumine suggestions merge|replace-main|adopt-thumbnail|adopt-title <suggestion-id> [--build <id>]",
|
|
797
|
+
"Usage: lumine suggestions [build] | lumine suggestions merge|replace-main|adopt-thumbnail|adopt-title|decline <suggestion-id> [--build <id>]",
|
|
769
798
|
);
|
|
770
799
|
}
|
|
771
800
|
|
|
801
|
+
function describeSuggestionForDecline(suggestion) {
|
|
802
|
+
if (suggestion.type === "title") {
|
|
803
|
+
return `name "${suggestion.suggestedTitle}"`;
|
|
804
|
+
}
|
|
805
|
+
if (suggestion.type === "thumbnail") return "thumbnail";
|
|
806
|
+
const branchNumber = Number(suggestion.branchNumber || 0);
|
|
807
|
+
return branchNumber
|
|
808
|
+
? `updates from branch ${branchNumber}`
|
|
809
|
+
: `updates from branch #${suggestion.branchBuildId}`;
|
|
810
|
+
}
|
|
811
|
+
|
|
772
812
|
async function resolveSuggestionRootBuildId(options, auth) {
|
|
773
813
|
const requestedBuildId = options.buildIdFlag
|
|
774
814
|
? resolveRequiredBuildId(options.buildIdFlag)
|
|
@@ -827,6 +867,9 @@ export function printBuildSuggestions({
|
|
|
827
867
|
console.log(
|
|
828
868
|
` replace: lumine suggestions replace-main ${suggestionId} --build ${rootBuildId}`,
|
|
829
869
|
);
|
|
870
|
+
console.log(
|
|
871
|
+
` decline: lumine suggestions decline ${suggestionId} --build ${rootBuildId}`,
|
|
872
|
+
);
|
|
830
873
|
continue;
|
|
831
874
|
}
|
|
832
875
|
if (suggestion.type === "title") {
|
|
@@ -838,6 +881,9 @@ export function printBuildSuggestions({
|
|
|
838
881
|
console.log(
|
|
839
882
|
` apply: lumine suggestions adopt-title ${suggestionId} --build ${rootBuildId}`,
|
|
840
883
|
);
|
|
884
|
+
console.log(
|
|
885
|
+
` decline: lumine suggestions decline ${suggestionId} --build ${rootBuildId}`,
|
|
886
|
+
);
|
|
841
887
|
continue;
|
|
842
888
|
}
|
|
843
889
|
console.log(
|
|
@@ -850,6 +896,9 @@ export function printBuildSuggestions({
|
|
|
850
896
|
console.log(
|
|
851
897
|
` apply: lumine suggestions adopt-thumbnail ${suggestionId} --build ${rootBuildId}`,
|
|
852
898
|
);
|
|
899
|
+
console.log(
|
|
900
|
+
` decline: lumine suggestions decline ${suggestionId} --build ${rootBuildId}`,
|
|
901
|
+
);
|
|
853
902
|
}
|
|
854
903
|
if (hasMore && nextCursor > 0) {
|
|
855
904
|
console.log(
|
|
@@ -2429,6 +2478,7 @@ export function parseArgs(args) {
|
|
|
2429
2478
|
"replace-main",
|
|
2430
2479
|
"adopt-thumbnail",
|
|
2431
2480
|
"adopt-title",
|
|
2481
|
+
"decline",
|
|
2432
2482
|
]);
|
|
2433
2483
|
const suggestionAction =
|
|
2434
2484
|
command === "suggest"
|
|
@@ -2970,6 +3020,7 @@ export function printHelp() {
|
|
|
2970
3020
|
lumine suggestions replace-main <suggestion-id> [--build <id>]
|
|
2971
3021
|
lumine suggestions adopt-thumbnail <suggestion-id> [--build <id>] [--yes]
|
|
2972
3022
|
lumine suggestions adopt-title <suggestion-id> [--build <id>] [--yes]
|
|
3023
|
+
lumine suggestions decline <suggestion-id> [--build <id>] [--yes]
|
|
2973
3024
|
lumine explore [search terms]
|
|
2974
3025
|
lumine select [twinkle-build-url]
|
|
2975
3026
|
lumine pull [twinkle-build-url]
|
|
@@ -3040,7 +3091,7 @@ export function printHelp() {
|
|
|
3040
3091
|
lumine admin reward-review list [--status pending|approved|all] [--cursor <id>] [--json]
|
|
3041
3092
|
lumine admin cardcraft-review list [--status pending|approved|all] [--cursor <id>] | show <id> | approve|reject|revoke <id> [--reason <text>]
|
|
3042
3093
|
lumine admin storage list [--status pending|approved|rejected|all] [--cursor <id>] | show <user-id-or-username> | approve <request-id> [--size <500MB|1GB|2GB|n MB>] [--reason <text>] | reject <request-id> [--reason <text>] | grant <user-id-or-username> --size <500MB|1GB|2GB|n MB> [--reason <text>]
|
|
3043
|
-
lumine admin meetup list [--status review|active|completed|all] | approve-crew <crewId> | approve-grownup <crewId> | emails [list|send <id>|discard <id>|review on|off] | friends [status|on|off] | parent-reply <crewId> <memberUserId> --note <text> | funnel [--days <1-90>] | branches [list [--status pending|official|rejected|all] | show <id> | approve <id> | reject <id> [--note <text>] | merge <id> --into <officialId>] | show <crewId> | approve-plan <crewId> [--note <text>] | send-back <crewId> --note <text> | approve <crewId> --attended <userId,userId,...> [--note <text>] | slot <crewId> --confirm <slotIndex> | resend-email <crewId> (Bridge Builder meetup quest)
|
|
3094
|
+
lumine admin meetup list [--status review|active|completed|all] | approve-crew <crewId> | approve-grownup <crewId> | emails [list|send <id>|discard <id>|review on|off] | friends [status|on|off] | parent-reply <crewId> <memberUserId> --note <text> | funnel [--days <1-90>] | branches [list [--status pending|official|rejected|all] | show <id> | approve <id> | reject <id> [--note <text>] | merge <id> --into <officialId>] | show <crewId> | approve-plan <crewId> [--note <text>] | send-back <crewId> --note <text> | approve <crewId> --attended <userId,userId,...> [--note <text>] | info <crewId> <userId> --decision ask|accept|ask-again|withdraw [--note <text>] | slot <crewId> --confirm <slotIndex> | resend-email <crewId> (Bridge Builder meetup quest)
|
|
3044
3095
|
lumine admin meetup story list | show <crewId> | approve <crewId> [--note <text>] [--no-announce] | send-back <crewId> --note <text> | unpublish <crewId> --note <text> (Bridge Builder story pages: publishes only what every tagged member's parent allowed)
|
|
3045
3096
|
lumine admin reward-activity [--date YYYY-MM-DD] [--days <1..31>] [--build <id>] [--json]
|
|
3046
3097
|
lumine admin chat-reports list [--status pending|open|reviewing|resolved|dismissed|all] [--cursor <id>] [--json]
|
package/lib/constants.js
CHANGED
|
@@ -238,8 +238,9 @@ lumine save --summary "Describe the change"
|
|
|
238
238
|
- On an owned canonical team project, \`lumine suggestions\` lists the owner's
|
|
239
239
|
currently open branch, thumbnail and name suggestions. Act on the exact
|
|
240
240
|
suggestion id it prints with \`lumine suggestions merge\`, \`replace-main\`,
|
|
241
|
-
\`adopt-thumbnail\` or \`
|
|
242
|
-
local branch state.
|
|
241
|
+
\`adopt-thumbnail\`, \`adopt-title\` or \`decline\`; do not infer an action
|
|
242
|
+
from stale local branch state. Decline only when the user asks to pass on
|
|
243
|
+
that suggestion.
|
|
243
244
|
|
|
244
245
|
## Assets (Runtime Media)
|
|
245
246
|
|
package/package.json
CHANGED
package/sdk/BUILD_SDK_INDEX.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Version: 1.63.0
|
|
4
4
|
Updated: 2026-09-28
|
|
5
|
-
Generated: 2026-10-
|
|
5
|
+
Generated: 2026-10-07T07:52:06.040Z
|
|
6
6
|
|
|
7
7
|
## Notes
|
|
8
8
|
- This SDK is injected into Build iframes via the Build preview/runtime.
|
|
@@ -257,6 +257,7 @@ files:read, media:read, media:write, live:read, live:write, user:read, users:rea
|
|
|
257
257
|
- title/body are accepted as aliases for label/summary.
|
|
258
258
|
- target is accepted as a backwards-compatible alias for launchTarget.
|
|
259
259
|
- Twinkle applies app API rate limits, a stricter notification emit rate limit, and existing notification mutes.
|
|
260
|
+
- Twinkle shows the acting member's username right before `summary` in the notification row, so write `summary` as a predicate ('posted in Lobby', 'responded to the Twinkle Newspaper'), never starting with the member's name (otherwise it reads 'Liki Liki responded…').
|
|
260
261
|
- Example: await Twinkle.notifications.notifySubscribers('room.message', { targetKey: 'room:lobby', eventKey: 'room.message.created', label: 'Room messages', summary: 'posted in Lobby', launchTarget: { view: 'room', roomId: 'lobby', messageId } });
|
|
261
262
|
- async getSubjectUpdateSubscription(subjectId) | scopes: notifications:read
|
|
262
263
|
- Returns: { subscription }
|